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