Skip to main content

miden_protocol/asset/vault/
mod.rs

1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use miden_crypto::merkle::InnerNodeInfo;
5
6use super::{
7    Asset,
8    AssetAmount,
9    AssetComposition,
10    ByteReader,
11    ByteWriter,
12    Deserializable,
13    DeserializationError,
14    FungibleAsset,
15    NonFungibleAsset,
16    Serializable,
17};
18use crate::Word;
19use crate::account::{AccountVaultDelta, NonFungibleDeltaAction};
20use crate::crypto::merkle::smt::{SMT_DEPTH, Smt};
21use crate::errors::{AssetError, AssetVaultError};
22
23mod partial;
24pub use partial::PartialVault;
25
26mod asset_witness;
27pub use asset_witness::AssetWitness;
28
29mod vault_key;
30pub use vault_key::AssetVaultKey;
31
32mod asset_id;
33pub use asset_id::AssetId;
34
35// ASSET VAULT
36// ================================================================================================
37
38/// A container for an unlimited number of assets.
39///
40/// An asset vault can contain an unlimited number of assets. The assets are stored in a Sparse
41/// Merkle tree as follows:
42/// - For fungible assets, the index of a node is defined by the issuing faucet ID, and the value of
43///   the node is the asset itself. Thus, for any fungible asset there will be only one node in the
44///   tree.
45/// - For non-fungible assets, the index is defined by the asset itself, and the asset is also the
46///   value of the node.
47///
48/// An asset vault can be reduced to a single hash which is the root of the Sparse Merkle Tree.
49#[derive(Debug, Clone, Default, PartialEq, Eq)]
50pub struct AssetVault {
51    asset_tree: Smt,
52}
53
54impl AssetVault {
55    // CONSTANTS
56    // --------------------------------------------------------------------------------------------
57
58    /// The depth of the SMT that represents the asset vault.
59    pub const DEPTH: u8 = SMT_DEPTH;
60
61    // CONSTRUCTOR
62    // --------------------------------------------------------------------------------------------
63
64    /// Returns a new [AssetVault] initialized with the provided assets.
65    pub fn new(assets: &[Asset]) -> Result<Self, AssetVaultError> {
66        Ok(Self {
67            asset_tree: Smt::with_entries(
68                assets.iter().map(|asset| (asset.vault_key().to_word(), asset.to_value_word())),
69            )
70            .map_err(AssetVaultError::DuplicateAsset)?,
71        })
72    }
73
74    // PUBLIC ACCESSORS
75    // --------------------------------------------------------------------------------------------
76
77    /// Returns the tree root of this vault.
78    pub fn root(&self) -> Word {
79        self.asset_tree.root()
80    }
81
82    /// Returns the asset corresponding to the provided asset vault key, or `None` if the asset
83    /// doesn't exist.
84    pub fn get(&self, asset_vault_key: AssetVaultKey) -> Option<Asset> {
85        let asset_value = self.asset_tree.get_value(&asset_vault_key.to_word());
86
87        if asset_value.is_empty() {
88            None
89        } else {
90            Some(
91                Asset::from_key_value(asset_vault_key, asset_value)
92                    .expect("asset vault should only store valid assets"),
93            )
94        }
95    }
96
97    /// Returns true if the specified non-fungible asset is stored in this vault.
98    pub fn has_non_fungible_asset(&self, asset: NonFungibleAsset) -> Result<bool, AssetVaultError> {
99        // check if the asset is stored in the vault
100        match self.asset_tree.get_value(&asset.vault_key().to_word()) {
101            asset if asset == Smt::EMPTY_VALUE => Ok(false),
102            _ => Ok(true),
103        }
104    }
105
106    /// Returns the balance of the fungible asset identified by `vault_key`.
107    ///
108    /// If the vault does not contain the asset, zero is returned.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if `vault_key`'s composition is not [`AssetComposition::Fungible`].
113    pub fn get_balance(&self, vault_key: AssetVaultKey) -> Result<AssetAmount, AssetError> {
114        if !vault_key.composition().is_fungible() {
115            return Err(AssetError::AssetCompositionMismatch {
116                faucet_id: vault_key.faucet_id(),
117                expected: AssetComposition::Fungible,
118                actual: vault_key.composition(),
119            });
120        }
121
122        let asset_value = self.asset_tree.get_value(&vault_key.to_word());
123        let asset = FungibleAsset::from_key_value(vault_key, asset_value)
124            .expect("asset vault should only store valid assets");
125
126        Ok(asset.amount())
127    }
128
129    /// Returns an iterator over the assets stored in the vault.
130    pub fn assets(&self) -> impl Iterator<Item = Asset> + '_ {
131        // SAFETY: The asset tree tracks only valid assets.
132        self.asset_tree.entries().map(|(key, value)| {
133            Asset::from_key_value_words(*key, *value)
134                .expect("asset vault should only store valid assets")
135        })
136    }
137
138    /// Returns an iterator over the inner nodes of the underlying [`Smt`].
139    pub fn inner_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
140        self.asset_tree.inner_nodes()
141    }
142
143    /// Returns an opening of the leaf associated with `vault_key`.
144    ///
145    /// The `vault_key` can be obtained with [`Asset::vault_key`].
146    pub fn open(&self, vault_key: AssetVaultKey) -> AssetWitness {
147        let smt_proof = self.asset_tree.open(&vault_key.to_word());
148        // SAFETY: The asset vault should only contain valid assets.
149        AssetWitness::new_unchecked(smt_proof)
150    }
151
152    /// Returns a bool indicating whether the vault is empty.
153    pub fn is_empty(&self) -> bool {
154        self.asset_tree.is_empty()
155    }
156
157    /// Returns the number of non-empty leaves in the underlying [`Smt`].
158    ///
159    /// Note that this may return a different value from [Self::num_assets()] as a single leaf may
160    /// contain more than one asset.
161    pub fn num_leaves(&self) -> usize {
162        self.asset_tree.num_leaves()
163    }
164
165    /// Returns the number of assets in this vault.
166    ///
167    /// Note that this may return a different value from [Self::num_leaves()] as a single leaf may
168    /// contain more than one asset.
169    pub fn num_assets(&self) -> usize {
170        self.asset_tree.num_entries()
171    }
172
173    // PUBLIC MODIFIERS
174    // --------------------------------------------------------------------------------------------
175
176    /// Applies the specified delta to the asset vault.
177    ///
178    /// # Errors
179    /// Returns an error:
180    /// - If the total value of the added assets is greater than [`FungibleAsset::MAX_AMOUNT`].
181    /// - If the delta contains an addition/subtraction for a fungible asset that is not stored in
182    ///   the vault.
183    /// - If the delta contains a non-fungible asset removal that is not stored in the vault.
184    /// - If the delta contains a non-fungible asset addition that is already stored in the vault.
185    /// - The maximum number of leaves per asset is exceeded.
186    pub fn apply_delta(&mut self, delta: &AccountVaultDelta) -> Result<(), AssetVaultError> {
187        for (vault_key, &delta) in delta.fungible().iter() {
188            // SAFETY: fungible asset delta should only contain fungible faucet IDs and delta amount
189            // should be in bounds
190            let asset = FungibleAsset::new(vault_key.faucet_id(), delta.unsigned_abs())
191                .expect("fungible asset delta should be valid")
192                .with_callbacks(vault_key.callback_flag());
193            match delta >= 0 {
194                true => self.add_fungible_asset(asset),
195                false => self.remove_fungible_asset(asset),
196            }?;
197        }
198
199        for (&asset, &action) in delta.non_fungible().iter() {
200            match action {
201                NonFungibleDeltaAction::Add => {
202                    self.add_non_fungible_asset(asset)?;
203                },
204                NonFungibleDeltaAction::Remove => {
205                    self.remove_non_fungible_asset(asset)?;
206                },
207            }
208        }
209
210        Ok(())
211    }
212
213    // ADD ASSET
214    // --------------------------------------------------------------------------------------------
215    /// Add the specified asset to the vault.
216    ///
217    /// # Errors
218    /// - If the total value of the added assets is greater than [`FungibleAsset::MAX_AMOUNT`].
219    /// - If the vault already contains the same non-fungible asset.
220    /// - The maximum number of leaves per asset is exceeded.
221    pub fn add_asset(&mut self, asset: Asset) -> Result<Asset, AssetVaultError> {
222        Ok(match asset {
223            Asset::Fungible(asset) => Asset::Fungible(self.add_fungible_asset(asset)?),
224            Asset::NonFungible(asset) => Asset::NonFungible(self.add_non_fungible_asset(asset)?),
225        })
226    }
227
228    /// Add the specified fungible asset to the vault. If the vault already contains an asset
229    /// issued by the same faucet, the amounts are added together.
230    ///
231    /// # Errors
232    /// - If the total value of the added assets is greater than [`FungibleAsset::MAX_AMOUNT`].
233    /// - The maximum number of leaves per asset is exceeded.
234    fn add_fungible_asset(
235        &mut self,
236        other_asset: FungibleAsset,
237    ) -> Result<FungibleAsset, AssetVaultError> {
238        let current_asset_value = self.asset_tree.get_value(&other_asset.vault_key().to_word());
239        let current_asset =
240            FungibleAsset::from_key_value(other_asset.vault_key(), current_asset_value)
241                .expect("asset vault should store valid assets");
242
243        let new_asset = current_asset
244            .add(other_asset)
245            .map_err(AssetVaultError::AddFungibleAssetBalanceError)?;
246
247        self.asset_tree
248            .insert(new_asset.vault_key().to_word(), new_asset.to_value_word())
249            .map_err(AssetVaultError::MaxLeafEntriesExceeded)?;
250
251        Ok(new_asset)
252    }
253
254    /// Add the specified non-fungible asset to the vault.
255    ///
256    /// # Errors
257    /// - If the vault already contains the same non-fungible asset.
258    /// - The maximum number of leaves per asset is exceeded.
259    fn add_non_fungible_asset(
260        &mut self,
261        asset: NonFungibleAsset,
262    ) -> Result<NonFungibleAsset, AssetVaultError> {
263        // add non-fungible asset to the vault
264        let old = self
265            .asset_tree
266            .insert(asset.vault_key().to_word(), asset.to_value_word())
267            .map_err(AssetVaultError::MaxLeafEntriesExceeded)?;
268
269        // if the asset already exists, return an error
270        if old != Smt::EMPTY_VALUE {
271            return Err(AssetVaultError::DuplicateNonFungibleAsset(asset));
272        }
273
274        Ok(asset)
275    }
276
277    // REMOVE ASSET
278    // --------------------------------------------------------------------------------------------
279    /// Remove the specified asset from the vault and returns the remaining asset, if any.
280    ///
281    /// - For fungible assets, returns `Some(Asset::Fungible(remaining))` with the remaining balance
282    ///   (which may have amount 0).
283    /// - For non-fungible assets, returns `None` since non-fungible assets are either fully present
284    ///   or absent.
285    ///
286    /// # Errors
287    /// - The fungible asset is not found in the vault.
288    /// - The amount of the fungible asset in the vault is less than the amount to be removed.
289    /// - The non-fungible asset is not found in the vault.
290    pub fn remove_asset(&mut self, asset: Asset) -> Result<Option<Asset>, AssetVaultError> {
291        match asset {
292            Asset::Fungible(asset) => {
293                let remaining = self.remove_fungible_asset(asset)?;
294                Ok(Some(Asset::Fungible(remaining)))
295            },
296            Asset::NonFungible(asset) => {
297                self.remove_non_fungible_asset(asset)?;
298                Ok(None)
299            },
300        }
301    }
302
303    /// Remove the specified fungible asset from the vault and returns the remaining fungible
304    /// asset. If the final amount of the asset is zero, the asset is removed from the vault.
305    ///
306    /// # Errors
307    /// - The asset is not found in the vault.
308    /// - The amount of the asset in the vault is less than the amount to be removed.
309    /// - The maximum number of leaves per asset is exceeded.
310    fn remove_fungible_asset(
311        &mut self,
312        other_asset: FungibleAsset,
313    ) -> Result<FungibleAsset, AssetVaultError> {
314        let current_asset_value = self.asset_tree.get_value(&other_asset.vault_key().to_word());
315        let current_asset =
316            FungibleAsset::from_key_value(other_asset.vault_key(), current_asset_value)
317                .expect("asset vault should store valid assets");
318
319        // If the asset's amount is 0, we consider it absent from the vault.
320        if current_asset.amount() == AssetAmount::ZERO {
321            return Err(AssetVaultError::FungibleAssetNotFound(other_asset));
322        }
323
324        let new_asset = current_asset
325            .sub(other_asset)
326            .map_err(AssetVaultError::SubtractFungibleAssetBalanceError)?;
327
328        // Note that if new_asset's amount is 0, its value's word representation is equal to
329        // the empty word, which results in the removal of the entire entry from the corresponding
330        // leaf.
331        #[cfg(debug_assertions)]
332        {
333            if new_asset.amount() == AssetAmount::ZERO {
334                assert!(new_asset.to_value_word().is_empty())
335            }
336        }
337
338        self.asset_tree
339            .insert(new_asset.vault_key().to_word(), new_asset.to_value_word())
340            .map_err(AssetVaultError::MaxLeafEntriesExceeded)?;
341
342        Ok(new_asset)
343    }
344
345    /// Remove the specified non-fungible asset from the vault.
346    ///
347    /// # Errors
348    /// - The non-fungible asset is not found in the vault.
349    /// - The maximum number of leaves per asset is exceeded.
350    fn remove_non_fungible_asset(
351        &mut self,
352        asset: NonFungibleAsset,
353    ) -> Result<(), AssetVaultError> {
354        // remove the asset from the vault.
355        let old = self
356            .asset_tree
357            .insert(asset.vault_key().to_word(), Smt::EMPTY_VALUE)
358            .map_err(AssetVaultError::MaxLeafEntriesExceeded)?;
359
360        // return an error if the asset did not exist in the vault.
361        if old == Smt::EMPTY_VALUE {
362            return Err(AssetVaultError::NonFungibleAssetNotFound(asset));
363        }
364
365        Ok(())
366    }
367}
368
369// SERIALIZATION
370// ================================================================================================
371
372impl Serializable for AssetVault {
373    fn write_into<W: ByteWriter>(&self, target: &mut W) {
374        let num_assets = self.asset_tree.num_entries();
375        target.write_usize(num_assets);
376        target.write_many(self.assets());
377    }
378
379    fn get_size_hint(&self) -> usize {
380        let mut size = 0;
381        let mut count: usize = 0;
382
383        for asset in self.assets() {
384            size += asset.get_size_hint();
385            count += 1;
386        }
387
388        size += count.get_size_hint();
389
390        size
391    }
392}
393
394impl Deserializable for AssetVault {
395    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
396        let num_assets = source.read_usize()?;
397        let assets = source.read_many_iter::<Asset>(num_assets)?.collect::<Result<Vec<_>, _>>()?;
398        Self::new(&assets).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
399    }
400}
401
402// TESTS
403// ================================================================================================
404
405#[cfg(test)]
406mod tests {
407    use assert_matches::assert_matches;
408
409    use super::*;
410
411    #[test]
412    fn vault_fails_on_absent_fungible_asset() {
413        let mut vault = AssetVault::default();
414        let err = vault.remove_asset(FungibleAsset::mock(50)).unwrap_err();
415        assert_matches!(err, AssetVaultError::FungibleAssetNotFound(_));
416    }
417}