Skip to main content

miden_client/rpc/domain/
account.rs

1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::vec::Vec;
3use core::fmt::{self, Debug, Formatter};
4
5use miden_protocol::account::{
6    Account, AccountCode, AccountHeader, AccountId, AccountStorage, AccountStorageHeader,
7    PartialAccount, PartialStorage, PartialStorageMap, StorageMap, StorageMapKey, StorageSlot,
8    StorageSlotHeader, StorageSlotName, StorageSlotType,
9};
10use miden_protocol::asset::{Asset, AssetId, AssetVault, PartialVault};
11use miden_protocol::block::account_tree::AccountWitness;
12use miden_protocol::crypto::merkle::SparseMerklePath;
13use miden_protocol::crypto::merkle::smt::PartialSmt;
14use miden_protocol::transaction::AccountInputs;
15use miden_objects::DecodeMessageExt;
16use miden_protocol::{EMPTY_WORD, Word};
17use miden_tx::utils::serde::{Deserializable, DeserializationError, Serializable};
18use thiserror::Error;
19
20use crate::alloc::string::ToString;
21use crate::rpc::{AccountStateAt, RpcError};
22use crate::rpc::domain::MissingFieldHelper;
23use crate::rpc::generated::rpc::get_account_request::account_detail_request::storage_map_detail_request::{MapKeys, SlotData};
24use crate::rpc::generated::rpc::get_account_request::account_detail_request::{
25    StorageMapDetailRequest, StorageMapDetailRequests, StorageRequest, VaultAssetIds, VaultRequest,
26};
27use crate::rpc::generated::{self as proto};
28
29// REGISTER ACCOUNT REQUEST
30// ================================================================================================
31
32/// Hides the invitation code, which is a secret that must not reach logs or error messages.
33impl Debug for proto::rpc::RegisterAccountRequest {
34    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
35        f.debug_struct("RegisterAccountRequest")
36            .field("account_id", &self.account_id)
37            .finish_non_exhaustive()
38    }
39}
40
41/// Hides the invitation code, which is a secret that must not reach logs or error messages.
42impl Debug for proto::rpc::IsInvitationCodeValidRequest {
43    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
44        f.debug_struct("IsInvitationCodeValidRequest").finish_non_exhaustive()
45    }
46}
47
48// FROM PROTO ACCOUNT HEADERS
49// ================================================================================================
50
51#[cfg(feature = "tonic")]
52impl proto::rpc::get_account_response::AccountDetails {
53    /// Converts the RPC response into `AccountDetails`.
54    ///
55    /// The RPC response may omit unchanged account codes. If so, this function uses
56    /// `known_account_codes` to fill in the missing code. If a required code cannot be found in the
57    /// response or `known_account_codes`, an error is returned.
58    ///
59    /// `fetch` specifies the requested storage keys and vault assets. The proofs must cover these
60    /// keys and assets and verify against the account's roots.
61    ///
62    /// # Errors
63    /// - If account code is missing both on `self` and `known_account_codes`
64    /// - If data cannot be correctly deserialized
65    /// - If a partial map does not cover exactly the keys requested for its slot
66    /// - If the vault details do not answer `fetch` for the account's vault root
67    pub fn into_domain(
68        self,
69        known_account_codes: &BTreeMap<Word, AccountCode>,
70        fetch: &AccountFetch,
71    ) -> Result<AccountDetails, crate::rpc::RpcError> {
72        use crate::rpc::RpcError;
73        use crate::rpc::domain::MissingFieldHelper;
74
75        let proto::rpc::get_account_response::AccountDetails {
76            header,
77            storage_details,
78            code,
79            vault_details,
80        } = self;
81        let header: AccountHeader = header
82            .ok_or(proto::rpc::get_account_response::AccountDetails::missing_field(stringify!(
83                header
84            )))?
85            .decode_and_verify()?;
86
87        let storage_details: AccountStorageDetails = storage_details
88            .ok_or(proto::rpc::get_account_response::AccountDetails::missing_field(stringify!(
89                storage_details
90            )))?
91            .try_into()?;
92
93        storage_details.validate_against_request(fetch.storage())?;
94
95        // If an account code was received, it means the previously known account code is no longer
96        // valid. If it was not, it means we sent a code commitment that matched and so our code is
97        // still valid
98        let code = {
99            let received_code: Option<AccountCode> =
100                code.map(DecodeMessageExt::decode_and_verify).transpose()?;
101            match received_code {
102                Some(code) => code,
103                None => known_account_codes
104                    .get(&header.code_commitment())
105                    .ok_or(RpcError::InvalidResponse(
106                        "Account code was not provided, but the response did not contain it either"
107                            .into(),
108                    ))?
109                    .clone(),
110            }
111        };
112
113        let vault_details = AccountVaultDetails::try_from(
114            vault_details
115                .ok_or(proto::rpc::AccountVaultDetails::missing_field(stringify!(vault_details)))?,
116        )?
117        .validate_against_request(header.vault_root(), fetch.vault())?;
118
119        Ok(AccountDetails {
120            header,
121            storage_details,
122            code,
123            vault_details,
124        })
125    }
126}
127
128// ACCOUNT DETAILS
129// ================================================================================================
130
131/// An account details.
132#[derive(Clone, Debug)]
133pub struct AccountDetails {
134    pub header: AccountHeader,
135    pub storage_details: AccountStorageDetails,
136    pub code: AccountCode,
137    pub vault_details: AccountVaultDetails,
138}
139
140impl TryFrom<AccountDetails> for PartialAccount {
141    type Error = RpcError;
142
143    /// Builds a partial account with the available vault and storage proofs.
144    ///
145    /// A map or vault that the node omitted or capped keeps its committed root. Reads fetch proofs
146    /// for that root during execution.
147    ///
148    /// # Errors
149    /// Returns an error if entries, assets, or proofs do not match the roots in the header, or if
150    /// the account requires a seed.
151    fn try_from(details: AccountDetails) -> Result<Self, Self::Error> {
152        let AccountDetails {
153            header,
154            storage_details,
155            code,
156            vault_details,
157        } = details;
158        let mut storage_maps = Vec::with_capacity(storage_details.map_details.len());
159        for map_details in storage_details.map_details {
160            let map = match map_details.entries {
161                StorageMapEntries::AllEntries(entries) => {
162                    let map = StorageMap::with_entries(
163                        entries.iter().map(|entry| (entry.key, entry.value)),
164                    )
165                    .map_err(|err| {
166                        RpcError::InvalidResponse(format!("invalid storage map entries: {err}"))
167                    })?;
168                    let slot_root = storage_details
169                        .header
170                        .slots()
171                        .find(|slot| *slot.name() == map_details.slot_name)
172                        .map(StorageSlotHeader::value);
173                    if Some(map.root()) != slot_root {
174                        return Err(RpcError::InvalidResponse(format!(
175                            "storage map entries for slot '{}' do not match the slot root",
176                            map_details.slot_name
177                        )));
178                    }
179                    PartialStorageMap::new_full(map)
180                },
181                StorageMapEntries::PartialMap { map_keys, partial_smt } => {
182                    PartialStorageMap::try_from_parts(partial_smt, map_keys).map_err(|err| {
183                        RpcError::InvalidResponse(format!("invalid storage map proof: {err}"))
184                    })?
185                },
186                StorageMapEntries::LimitExceeded => continue,
187            };
188            storage_maps.push(map);
189        }
190        let vault = match vault_details {
191            AccountVaultDetails::Assets(assets) => {
192                let vault = AssetVault::new(&assets).map_err(|err| {
193                    RpcError::InvalidResponse(format!("invalid asset list: {err}"))
194                })?;
195                if vault.root() == header.vault_root() {
196                    PartialVault::new_full(vault)
197                } else if assets.is_empty() {
198                    // The node sends no assets when the request skips the vault or sends its
199                    // current root.
200                    PartialVault::new(header.vault_root())
201                } else {
202                    return Err(RpcError::InvalidResponse(
203                        "asset list does not match the vault root".into(),
204                    ));
205                }
206            },
207            AccountVaultDetails::Partial(vault) => {
208                if vault.root() != header.vault_root() {
209                    return Err(RpcError::InvalidResponse(
210                        "vault proofs do not match the vault root".into(),
211                    ));
212                }
213                vault
214            },
215            AccountVaultDetails::TooManyAssets => PartialVault::new(header.vault_root()),
216        };
217        let storage = PartialStorage::new(storage_details.header, storage_maps)
218            .map_err(|err| RpcError::InvalidResponse(format!("invalid partial storage: {err}")))?;
219        let account = PartialAccount::new(header.id(), header.nonce(), code, storage, vault, None)
220            .map_err(|err| RpcError::InvalidResponse(format!("invalid partial account: {err}")))?;
221        if account.to_commitment() != header.to_commitment() {
222            return Err(RpcError::InvalidResponse(
223                "partial account does not match its header".into(),
224            ));
225        }
226        Ok(account)
227    }
228}
229
230impl TryFrom<&AccountDetails> for Account {
231    type Error = RpcError;
232
233    /// Builds an [`Account`] from [`AccountDetails`].
234    ///
235    /// This conversion fails if the account details are incomplete, i.e., when the account's
236    /// storage maps or vault exceed the node's size threshold, or when only specific map keys or
237    /// vault assets were requested.
238    fn try_from(details: &AccountDetails) -> Result<Self, Self::Error> {
239        let assets = match &details.vault_details {
240            AccountVaultDetails::Assets(assets) => assets,
241            AccountVaultDetails::TooManyAssets => {
242                return Err(RpcError::ExpectedDataMissing(
243                    "cannot build account: vault has too many assets".into(),
244                ));
245            },
246            AccountVaultDetails::Partial(_) => {
247                return Err(RpcError::ExpectedDataMissing(
248                    "cannot build account: only specific vault assets were requested".into(),
249                ));
250            },
251        };
252
253        if let Some(slot_name) = details
254            .storage_details
255            .map_details
256            .iter()
257            .find(|m| m.is_limit_exceeded())
258            .map(|m| &m.slot_name)
259        {
260            return Err(RpcError::ExpectedDataMissing(format!(
261                "cannot build account: storage map slot '{slot_name}' has too many entries",
262            )));
263        }
264
265        let mut slots: Vec<StorageSlot> = Vec::new();
266
267        for slot_header in details.storage_details.header.slots() {
268            match slot_header.slot_type() {
269                StorageSlotType::Value => {
270                    slots.push(StorageSlot::with_value(
271                        slot_header.name().clone(),
272                        slot_header.value(),
273                    ));
274                },
275                StorageSlotType::Map => {
276                    let map_details = details
277                        .storage_details
278                        .find_map_details(slot_header.name())
279                        .ok_or_else(|| {
280                            RpcError::ExpectedDataMissing(format!(
281                                "slot '{}' is a map but has no map_details in response",
282                                slot_header.name()
283                            ))
284                        })?;
285
286                    let storage_map = map_details
287                        .entries
288                        .clone()
289                        .into_storage_map()
290                        .ok_or_else(|| {
291                            RpcError::ExpectedDataMissing(format!(
292                                "slot '{}' did not come back with all its entries, so the full \
293                                 account cannot be built",
294                                slot_header.name(),
295                            ))
296                        })?
297                        .map_err(|err| {
298                            RpcError::InvalidResponse(format!(
299                                "the rpc api returned a non-valid map entry: {err}"
300                            ))
301                        })?;
302
303                    slots.push(StorageSlot::with_map(slot_header.name().clone(), storage_map));
304                },
305            }
306        }
307
308        let asset_vault = AssetVault::new(assets).map_err(|err| {
309            RpcError::InvalidResponse(format!("rpc api returned non-valid assets: {err}"))
310        })?;
311
312        let account_storage = AccountStorage::new(slots).map_err(|err| {
313            RpcError::InvalidResponse(format!("rpc api returned non-valid storage slots: {err}"))
314        })?;
315
316        Account::new(
317            details.header.id(),
318            asset_vault,
319            account_storage,
320            details.code.clone(),
321            details.header.nonce(),
322            None,
323        )
324        .map_err(|err| {
325            RpcError::InvalidResponse(format!(
326                "failed to construct account from rpc api response: {err}"
327            ))
328        })
329    }
330}
331
332// ACCOUNT STORAGE DETAILS
333// ================================================================================================
334
335/// Account storage details for `AccountResponse`
336#[derive(Clone, Debug)]
337pub struct AccountStorageDetails {
338    /// Account storage header (storage slot info for up to 256 slots)
339    pub header: AccountStorageHeader,
340    /// Additional data for the requested storage maps
341    pub map_details: Vec<AccountStorageMapDetails>,
342}
343
344impl AccountStorageDetails {
345    /// Find the matching details for a map, given its storage slot name.
346    //  This linear search should be good enough since there can be
347    //  only up to 256 slots, so locality probably wins here.
348    pub fn find_map_details(&self, target: &StorageSlotName) -> Option<&AccountStorageMapDetails> {
349        self.map_details.iter().find(|map_detail| map_detail.slot_name == *target)
350    }
351
352    /// Checks that every partial map covers exactly the keys that were requested for its slot.
353    ///
354    /// # Errors
355    /// - If a partial map does not cover a key that was requested for its slot.
356    /// - If a partial map covers a key that was not requested.
357    pub fn validate_against_request(&self, storage: &StorageMapFetch) -> Result<(), RpcError> {
358        for map_detail in &self.map_details {
359            let StorageMapEntries::PartialMap { map_keys, .. } = &map_detail.entries else {
360                continue;
361            };
362
363            let requested_keys = match storage {
364                StorageMapFetch::Slots(requirements) => {
365                    requirements.keys_for_slot(&map_detail.slot_name)
366                },
367                StorageMapFetch::Skip | StorageMapFetch::All => &[],
368            };
369            if let Some(key) = requested_keys.iter().find(|key| !map_keys.contains(key)) {
370                return Err(RpcError::InvalidResponse(format!(
371                    "partial storage map for slot '{}' does not cover requested key {}",
372                    map_detail.slot_name,
373                    key.to_hex(),
374                )));
375            }
376            if let Some(key) = map_keys.iter().find(|key| !requested_keys.contains(key)) {
377                return Err(RpcError::InvalidResponse(format!(
378                    "partial storage map for slot '{}' covers key {}, which was not requested",
379                    map_detail.slot_name,
380                    key.to_hex(),
381                )));
382            }
383        }
384
385        Ok(())
386    }
387}
388
389impl TryFrom<proto::rpc::AccountStorageDetails> for AccountStorageDetails {
390    type Error = RpcError;
391
392    fn try_from(value: proto::rpc::AccountStorageDetails) -> Result<Self, Self::Error> {
393        let header: AccountStorageHeader = value
394            .header
395            .ok_or(proto::account::AccountStorageHeader::missing_field(stringify!(header)))?
396            .decode_and_verify()?;
397        let map_details = value
398            .map_details
399            .into_iter()
400            .map(core::convert::TryInto::try_into)
401            .collect::<Result<Vec<AccountStorageMapDetails>, RpcError>>()?;
402
403        // A partial map is only worth anything if it is anchored to the slot root the account
404        // commitment covers. Without this check the node could serve a self-consistent tree of its
405        // own making.
406        for map_detail in &map_details {
407            let StorageMapEntries::PartialMap { partial_smt, .. } = &map_detail.entries else {
408                continue;
409            };
410
411            let slot = header
412                .slots()
413                .find(|slot| *slot.name() == map_detail.slot_name)
414                .ok_or_else(|| {
415                    RpcError::InvalidResponse(format!(
416                        "partial storage map references slot '{}', which is absent from the \
417                         storage header",
418                        map_detail.slot_name,
419                    ))
420                })?;
421            if slot.slot_type() != StorageSlotType::Map {
422                return Err(RpcError::InvalidResponse(format!(
423                    "partial storage map references slot '{}', which is not a map",
424                    map_detail.slot_name,
425                )));
426            }
427            if partial_smt.root() != slot.value() {
428                return Err(RpcError::InvalidResponse(format!(
429                    "partial storage map for slot '{}' has root {} but the storage header reports \
430                     {}",
431                    map_detail.slot_name,
432                    partial_smt.root(),
433                    slot.value(),
434                )));
435            }
436        }
437
438        Ok(Self { header, map_details })
439    }
440}
441
442// ACCOUNT MAP DETAILS
443// ================================================================================================
444
445#[derive(Clone, Debug)]
446pub struct AccountStorageMapDetails {
447    /// Storage slot name of the storage map.
448    pub slot_name: StorageSlotName,
449    /// The map data the node returned for this slot. The variants are mutually exclusive.
450    pub entries: StorageMapEntries,
451}
452
453impl AccountStorageMapDetails {
454    /// The maximum number of keys the node will cover with a single partial map. The node counts
455    /// this across all slots of a request, so honouring it per slot is a conservative bound.
456    pub const MAX_PARTIAL_MAP_KEYS: usize = 64;
457
458    /// Returns `true` when the node reported that this slot has more entries than it will return in
459    /// a single response, meaning the entries have to be fetched through
460    /// [`crate::rpc::NodeRpcClient::sync_storage_maps`] instead.
461    pub fn is_limit_exceeded(&self) -> bool {
462        matches!(self.entries, StorageMapEntries::LimitExceeded)
463    }
464}
465
466impl TryFrom<proto::rpc::account_storage_details::AccountStorageMapDetails>
467    for AccountStorageMapDetails
468{
469    type Error = RpcError;
470
471    fn try_from(
472        value: proto::rpc::account_storage_details::AccountStorageMapDetails,
473    ) -> Result<Self, Self::Error> {
474        use proto::rpc::account_storage_details::account_storage_map_details::Result as ProtoResult;
475
476        let slot_name = StorageSlotName::new(value.slot_name)
477            .map_err(|err| RpcError::ExpectedDataMissing(err.to_string()))?;
478
479        let entries = match value.result {
480            Some(ProtoResult::TooManyEntries(true)) => StorageMapEntries::LimitExceeded,
481            Some(ProtoResult::TooManyEntries(false)) => {
482                return Err(RpcError::InvalidResponse(
483                    "too_many_entries must be true when set".into(),
484                ));
485            },
486            Some(ProtoResult::AllEntries(all_entries)) => {
487                let entries = all_entries
488                    .entries
489                    .into_iter()
490                    .map(core::convert::TryInto::try_into)
491                    .collect::<Result<Vec<StorageMapEntry>, RpcError>>()?;
492                StorageMapEntries::AllEntries(entries)
493            },
494            Some(ProtoResult::PartialMap(partial_map)) => {
495                if partial_map.map_keys.len() > Self::MAX_PARTIAL_MAP_KEYS {
496                    return Err(RpcError::InvalidResponse(format!(
497                        "partial storage map for slot '{slot_name}' contains {} keys, exceeding \
498                         the limit of {}",
499                        partial_map.map_keys.len(),
500                        Self::MAX_PARTIAL_MAP_KEYS,
501                    )));
502                }
503
504                let map_keys = partial_map
505                    .map_keys
506                    .into_iter()
507                    .map(|key| Word::try_from(key).map(StorageMapKey::new))
508                    .collect::<Result<Vec<_>, _>>()?;
509                if let Some(key) = first_duplicate_key(&map_keys) {
510                    return Err(RpcError::InvalidResponse(format!(
511                        "partial storage map for slot '{slot_name}' repeats key {}",
512                        key.to_hex(),
513                    )));
514                }
515
516                let partial_smt: PartialSmt = partial_map
517                        .partial_smt
518                        .ok_or(proto::rpc::account_storage_details::account_storage_map_details::PartialStorageMap::missing_field(
519                            stringify!(partial_smt),
520                        ))?
521                        .decode_and_verify()?;
522
523                // The response sends the values only inside the tree, so a key the tree does not
524                // track carries no value at all and would fail later, at read time.
525                for key in &map_keys {
526                    partial_smt.get_value(&key.hash().as_word()).map_err(|_| {
527                        RpcError::InvalidResponse(format!(
528                            "partial storage map for slot '{slot_name}' does not track key {}",
529                            key.to_hex(),
530                        ))
531                    })?;
532                }
533
534                StorageMapEntries::PartialMap { map_keys, partial_smt }
535            },
536            None => {
537                return Err(RpcError::InvalidResponse(format!(
538                    "storage map details for slot '{slot_name}' carry no result",
539                )));
540            },
541        };
542
543        Ok(Self { slot_name, entries })
544    }
545}
546
547/// Returns the first key that appears more than once, if any.
548///
549/// The key lists this guards are bounded by [`AccountStorageMapDetails::MAX_PARTIAL_MAP_KEYS`], so
550/// the quadratic scan avoids allocating a set.
551fn first_duplicate_key(keys: &[StorageMapKey]) -> Option<&StorageMapKey> {
552    keys.iter()
553        .enumerate()
554        .find_map(|(index, key)| keys[..index].contains(key).then_some(key))
555}
556
557// STORAGE MAP ENTRY
558// ================================================================================================
559
560/// A storage map entry containing a key-value pair.
561#[derive(Clone, Debug)]
562pub struct StorageMapEntry {
563    pub key: StorageMapKey,
564    pub value: Word,
565}
566
567impl TryFrom<proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry>
568    for StorageMapEntry
569{
570    type Error = RpcError;
571
572    fn try_from(value: proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry) -> Result<Self, Self::Error> {
573        let key = Word::try_from(value.key.ok_or(RpcError::ExpectedDataMissing("key".into()))?)
574            .map(StorageMapKey::new)?;
575        let value = value.value.ok_or(RpcError::ExpectedDataMissing("value".into()))?.try_into()?;
576        Ok(Self { key, value })
577    }
578}
579
580// STORAGE MAP ENTRIES
581// ================================================================================================
582
583/// The map data a `/GetAccount` response carries for one storage map slot. The variants are
584/// mutually exclusive, mirroring the node's response.
585#[derive(Clone, Debug)]
586pub enum StorageMapEntries {
587    /// The slot has more entries than the node returns in a single response. No entries are
588    /// carried; fetch them with [`crate::rpc::NodeRpcClient::sync_storage_maps`].
589    LimitExceeded,
590    /// All entries in the storage map (no proofs needed as the full map is available).
591    AllEntries(Vec<StorageMapEntry>),
592    /// The specific keys that were requested, covered by a single partial SMT.
593    ///
594    /// The values are carried only inside `partial_smt`: read one by hashing its raw key and
595    /// calling [`PartialSmt::get_value`]. Every key in `map_keys` is guaranteed to be tracked by
596    /// `partial_smt`, so such a read cannot fail.
597    PartialMap {
598        /// The original, unhashed keys covered by `partial_smt`.
599        map_keys: Vec<StorageMapKey>,
600        /// The partial SMT proving the value of every key in `map_keys`.
601        partial_smt: PartialSmt,
602    },
603}
604
605impl StorageMapEntries {
606    /// Converts the entries into a [`StorageMap`].
607    ///
608    /// Returns `None` for every variant other than [`AllEntries`](Self::AllEntries), since only
609    /// that one carries the whole map.
610    pub fn into_storage_map(
611        self,
612    ) -> Option<Result<StorageMap, miden_protocol::errors::StorageMapError>> {
613        match self {
614            StorageMapEntries::AllEntries(entries) => {
615                Some(StorageMap::with_entries(entries.into_iter().map(|e| (e.key, e.value))))
616            },
617            StorageMapEntries::LimitExceeded | StorageMapEntries::PartialMap { .. } => None,
618        }
619    }
620}
621
622// ACCOUNT VAULT DETAILS
623// ================================================================================================
624
625/// The vault data that a `/GetAccount` response carries for an account. The variants are mutually
626/// exclusive.
627#[derive(Clone, Debug)]
628pub enum AccountVaultDetails {
629    /// The vault has more assets than the node returns in a single response. No assets are carried.
630    /// Fetch them with [`crate::rpc::NodeRpcClient::sync_account_vault`].
631    TooManyAssets,
632    /// The assets in the account's vault.
633    ///
634    /// The list is empty if the request skips the vault, or if the vault root is equal to the
635    /// commitment in [`VaultFetch::IfChangedFrom`]. Thus, an empty list does not show that the
636    /// vault is empty.
637    Assets(Vec<Asset>),
638    /// Proofs for the assets requested with [`VaultFetch::Assets`]. The proofs also cover requested
639    /// assets that are not in the vault.
640    Partial(PartialVault),
641}
642
643impl AccountVaultDetails {
644    /// Returns `true` if the vault has more assets than the node returns in a single response.
645    pub fn is_too_many_assets(&self) -> bool {
646        matches!(self, Self::TooManyAssets)
647    }
648
649    /// Returns the asset list, or `None` for every variant other than [`Assets`](Self::Assets).
650    pub fn assets(&self) -> Option<&[Asset]> {
651        match self {
652            Self::Assets(assets) => Some(assets),
653            Self::TooManyAssets | Self::Partial(_) => None,
654        }
655    }
656
657    /// Converts the details into the vault proofs, or returns `None` for every variant other than
658    /// [`Partial`](Self::Partial).
659    pub fn into_partial_vault(self) -> Option<PartialVault> {
660        match self {
661            Self::Partial(partial_vault) => Some(partial_vault),
662            Self::TooManyAssets | Self::Assets(_) => None,
663        }
664    }
665
666    /// Checks the vault details against the request they answer and against `vault_root`, the vault
667    /// root in the account header.
668    ///
669    /// The node can answer a [`VaultFetch::Assets`] request with the full vault contents instead of
670    /// proofs. This function converts that answer into a partial vault. Thus, for an asset request,
671    /// the result is always [`Partial`](Self::Partial).
672    ///
673    /// # Errors
674    /// - If the details hold proofs for a request other than [`VaultFetch::Assets`]
675    /// - If the vault has too many assets to answer a [`VaultFetch::Assets`] request
676    /// - If the proofs or the full vault contents do not verify against `vault_root`
677    /// - If the proofs do not cover all requested assets
678    /// - If the proofs track an asset that was not requested
679    #[cfg(feature = "tonic")]
680    pub(crate) fn validate_against_request(
681        self,
682        vault_root: Word,
683        vault_request: &VaultFetch,
684    ) -> Result<Self, RpcError> {
685        let VaultFetch::Assets(asset_ids) = vault_request else {
686            if matches!(self, Self::Partial(_)) {
687                return Err(RpcError::InvalidResponse(
688                    "the response contains vault proofs that were not requested".into(),
689                ));
690            }
691            return Ok(self);
692        };
693
694        let partial_vault = match self {
695            Self::Partial(partial_vault) => {
696                if let Some(asset_id) =
697                    partial_vault.asset_ids().find(|asset_id| !asset_ids.contains(asset_id))
698                {
699                    return Err(RpcError::InvalidResponse(format!(
700                        "the vault proofs track asset {asset_id}, which was not requested"
701                    )));
702                }
703                partial_vault
704            },
705            Self::TooManyAssets => {
706                return Err(RpcError::InvalidResponse(
707                    "the node did not prove the requested assets because the vault is too large"
708                        .into(),
709                ));
710            },
711            Self::Assets(assets) => {
712                PartialVault::new_full(AssetVault::new(&assets).map_err(|err| {
713                    RpcError::InvalidResponse(format!("rpc api returned non-valid assets: {err}"))
714                })?)
715            },
716        };
717
718        if partial_vault.root() != vault_root {
719            return Err(RpcError::InvalidResponse(format!(
720                "the vault proofs verify against root {} but the account header has vault root \
721                 {vault_root}",
722                partial_vault.root(),
723            )));
724        }
725
726        if let Some(asset_id) =
727            asset_ids.iter().find(|asset_id| partial_vault.open(**asset_id).is_err())
728        {
729            return Err(RpcError::InvalidResponse(format!(
730                "the vault proofs do not cover requested asset {asset_id}"
731            )));
732        }
733
734        Ok(Self::Partial(partial_vault))
735    }
736}
737
738impl TryFrom<proto::rpc::AccountVaultDetails> for AccountVaultDetails {
739    type Error = RpcError;
740
741    /// Converts the vault details of a `/GetAccount` response.
742    ///
743    /// The response must set only one of the asset list, the vault proofs, and the too many assets
744    /// flag. A response that sets none of them converts to an empty asset list.
745    fn try_from(value: proto::rpc::AccountVaultDetails) -> Result<Self, Self::Error> {
746        let proto::rpc::AccountVaultDetails { too_many_assets, assets, partial_vault } = value;
747
748        match (too_many_assets, assets.is_empty(), partial_vault) {
749            (false, _, None) => Ok(Self::Assets(
750                assets
751                    .into_iter()
752                    .map(DecodeMessageExt::decode_and_verify)
753                    .collect::<Result<Vec<Asset>, _>>()?,
754            )),
755            (true, true, None) => Ok(Self::TooManyAssets),
756            (false, true, Some(partial_vault)) => Ok(Self::Partial(partial_vault.try_into()?)),
757            _ => Err(RpcError::InvalidResponse(
758                "the vault details set more than one of the asset list, the vault proofs, and the \
759                 too many assets flag"
760                    .into(),
761            )),
762        }
763    }
764}
765
766impl TryFrom<proto::rpc::account_vault_details::PartialVault> for PartialVault {
767    type Error = RpcError;
768
769    /// Converts the vault proofs of a `/GetAccount` response.
770    ///
771    /// [`PartialVault::try_from_parts`] rejects duplicate asset IDs and asset IDs that the partial
772    /// SMT does not track.
773    fn try_from(
774        value: proto::rpc::account_vault_details::PartialVault,
775    ) -> Result<Self, Self::Error> {
776        let asset_ids = value
777            .asset_ids
778            .into_iter()
779            .map(DecodeMessageExt::decode_and_verify)
780            .collect::<Result<Vec<AssetId>, _>>()?;
781
782        let partial_smt: PartialSmt = value
783            .partial_smt
784            .ok_or(proto::rpc::account_vault_details::PartialVault::missing_field(stringify!(
785                partial_smt
786            )))?
787            .decode_and_verify()?;
788
789        PartialVault::try_from_parts(partial_smt, asset_ids).map_err(|err| {
790            RpcError::InvalidResponse(format!("rpc api returned a non-valid partial vault: {err}"))
791        })
792    }
793}
794
795// ACCOUNT PROOF
796// ================================================================================================
797
798/// Represents a proof of existence of an account's state at a specific block number.
799#[derive(Clone, Debug)]
800pub struct AccountProof {
801    /// Account witness.
802    account_witness: AccountWitness,
803    /// State headers of public accounts.
804    state_headers: Option<AccountDetails>,
805}
806
807impl AccountProof {
808    /// Converts public account details and their witness into execution inputs.
809    ///
810    /// # Errors
811    /// Returns an error if account details are absent or cannot form a partial account.
812    pub fn into_account_inputs(self) -> Result<AccountInputs, RpcError> {
813        let (witness, details) = self.into_parts();
814        let details = details
815            .ok_or_else(|| RpcError::ExpectedDataMissing("public account details".into()))?;
816        Ok(AccountInputs::new(details.try_into()?, witness))
817    }
818
819    /// Creates a new [`AccountProof`].
820    pub fn new(
821        account_witness: AccountWitness,
822        account_details: Option<AccountDetails>,
823    ) -> Result<Self, AccountProofError> {
824        if let Some(AccountDetails {
825            header: account_header,
826            storage_details: _,
827            code,
828            ..
829        }) = &account_details
830        {
831            if account_header.to_commitment() != account_witness.state_commitment() {
832                return Err(AccountProofError::InconsistentAccountCommitment);
833            }
834            if account_header.id() != account_witness.id() {
835                return Err(AccountProofError::InconsistentAccountId);
836            }
837            if code.commitment() != account_header.code_commitment() {
838                return Err(AccountProofError::InconsistentCodeCommitment);
839            }
840        }
841
842        Ok(Self {
843            account_witness,
844            state_headers: account_details,
845        })
846    }
847
848    /// Returns the account ID related to the account proof.
849    pub fn account_id(&self) -> AccountId {
850        self.account_witness.id()
851    }
852
853    /// Returns the account header, if present.
854    pub fn account_header(&self) -> Option<&AccountHeader> {
855        self.state_headers.as_ref().map(|account_details| &account_details.header)
856    }
857
858    /// Returns the storage header, if present.
859    pub fn storage_header(&self) -> Option<&AccountStorageHeader> {
860        self.state_headers
861            .as_ref()
862            .map(|account_details| &account_details.storage_details.header)
863    }
864
865    /// Returns the full storage details, if available (public accounts only).
866    pub fn storage_details(&self) -> Option<&AccountStorageDetails> {
867        self.state_headers.as_ref().map(|d| &d.storage_details)
868    }
869
870    /// Returns the vault details, if available (public accounts only).
871    pub fn vault_details(&self) -> Option<&AccountVaultDetails> {
872        self.state_headers.as_ref().map(|d| &d.vault_details)
873    }
874
875    /// Returns the storage map details for a specific slot, if available.
876    pub fn find_map_details(
877        &self,
878        slot_name: &StorageSlotName,
879    ) -> Option<&AccountStorageMapDetails> {
880        self.state_headers
881            .as_ref()
882            .and_then(|details| details.storage_details.find_map_details(slot_name))
883    }
884
885    /// Returns the account code, if present.
886    pub fn account_code(&self) -> Option<&AccountCode> {
887        self.state_headers.as_ref().map(|headers| &headers.code)
888    }
889
890    /// Returns the code commitment, if account code is present in the state headers.
891    pub fn code_commitment(&self) -> Option<Word> {
892        self.account_code().map(AccountCode::commitment)
893    }
894
895    /// Returns the current state commitment of the account.
896    pub fn account_commitment(&self) -> Word {
897        self.account_witness.state_commitment()
898    }
899
900    pub fn account_witness(&self) -> &AccountWitness {
901        &self.account_witness
902    }
903
904    /// Returns the proof of the account's inclusion.
905    pub fn merkle_proof(&self) -> &SparseMerklePath {
906        self.account_witness.path()
907    }
908
909    /// Deconstructs `AccountProof` into its individual parts.
910    pub fn into_parts(self) -> (AccountWitness, Option<AccountDetails>) {
911        (self.account_witness, self.state_headers)
912    }
913
914    /// Consumes the proof and returns the account details, if present (public accounts only).
915    pub fn into_details(self) -> Option<AccountDetails> {
916        self.state_headers
917    }
918
919    /// Mutable accessor for the account details, when present.
920    ///
921    /// Useful for resolving oversized vault or storage data in place via
922    /// [`crate::rpc::NodeRpcClient::resolve_oversize_vault`] and
923    /// [`crate::rpc::NodeRpcClient::resolve_oversize_storage_maps`].
924    pub fn details_mut(&mut self) -> Option<&mut AccountDetails> {
925        self.state_headers.as_mut()
926    }
927}
928
929#[cfg(feature = "tonic")]
930impl TryFrom<proto::rpc::GetAccountResponse> for AccountProof {
931    type Error = RpcError;
932    fn try_from(account_proof: proto::rpc::GetAccountResponse) -> Result<Self, Self::Error> {
933        let Some(witness) = account_proof.witness else {
934            return Err(RpcError::ExpectedDataMissing(
935                "GetAccount returned an account without witness".to_string(),
936            ));
937        };
938
939        let details: Option<AccountDetails> = {
940            match account_proof.details {
941                None => None,
942                Some(details) => {
943                    Some(details.into_domain(&BTreeMap::new(), &AccountFetch::default())?)
944                },
945            }
946        };
947        AccountProof::new(witness.decode_and_verify()?, details)
948            .map_err(|err| RpcError::InvalidResponse(format!("{err}")))
949    }
950}
951
952// ACCOUNT STORAGE REQUEST
953// ================================================================================================
954
955/// Per-slot map data to include in a `/GetAccount` response. Slots absent here are omitted from
956/// `map_details` (the storage header still lists every slot).
957///
958/// - Empty key list: all entries, no proof. May come back as [`StorageMapEntries::LimitExceeded`].
959/// - Non-empty key list: just those keys, covered by one partial SMT.
960#[derive(Clone, Debug, Default, Eq, PartialEq)]
961pub struct AccountStorageRequirements(BTreeMap<StorageSlotName, Vec<StorageMapKey>>);
962
963impl AccountStorageRequirements {
964    /// Requests the specified keys per slot, covered by one partial SMT per slot. An empty key
965    /// iterator for a slot behaves like [`Self::all_entries`].
966    ///
967    /// Repeated keys within a slot are collapsed, since the node rejects a request that names the
968    /// same key twice.
969    pub fn new<'a>(
970        slots_and_keys: impl IntoIterator<
971            Item = (StorageSlotName, impl IntoIterator<Item = &'a StorageMapKey>),
972        >,
973    ) -> Self {
974        let map = slots_and_keys
975            .into_iter()
976            .map(|(slot_name, keys_iter)| {
977                let mut keys_vec: Vec<StorageMapKey> = Vec::new();
978                for key in keys_iter {
979                    if !keys_vec.contains(key) {
980                        keys_vec.push(*key);
981                    }
982                }
983                (slot_name, keys_vec)
984            })
985            .collect();
986
987        AccountStorageRequirements(map)
988    }
989
990    /// Requests every entry of each given slot, without a proof. Oversize maps come back as
991    /// [`StorageMapEntries::LimitExceeded`].
992    pub fn all_entries(slot_names: &[StorageSlotName]) -> Self {
993        AccountStorageRequirements(
994            slot_names.iter().map(|name| (name.clone(), Vec::new())).collect(),
995        )
996    }
997
998    pub fn inner(&self) -> &BTreeMap<StorageSlotName, Vec<StorageMapKey>> {
999        &self.0
1000    }
1001
1002    /// Returns the keys requested for a given slot, or an empty slice if none were specified.
1003    pub fn keys_for_slot(&self, slot_name: &StorageSlotName) -> &[StorageMapKey] {
1004        self.0.get(slot_name).map_or(&[], Vec::as_slice)
1005    }
1006}
1007
1008impl From<AccountStorageRequirements> for Vec<StorageMapDetailRequest> {
1009    fn from(value: AccountStorageRequirements) -> Vec<StorageMapDetailRequest> {
1010        let request_map = value.0;
1011        let mut requests = Vec::with_capacity(request_map.len());
1012        for (slot_name, map_keys) in request_map {
1013            let slot_data = if map_keys.is_empty() {
1014                Some(SlotData::AllEntries(true))
1015            } else {
1016                let keys = map_keys.into_iter().map(|key| Word::from(key).into()).collect();
1017                Some(SlotData::MapKeys(MapKeys { map_keys: keys }))
1018            };
1019            requests.push(StorageMapDetailRequest {
1020                slot_name: slot_name.to_string(),
1021                slot_data,
1022            });
1023        }
1024        requests
1025    }
1026}
1027
1028impl Serializable for AccountStorageRequirements {
1029    fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
1030        target.write(&self.0);
1031    }
1032}
1033
1034impl Deserializable for AccountStorageRequirements {
1035    fn read_from<R: miden_tx::utils::serde::ByteReader>(
1036        source: &mut R,
1037    ) -> Result<Self, miden_tx::utils::serde::DeserializationError> {
1038        Ok(AccountStorageRequirements(source.read()?))
1039    }
1040}
1041
1042// GET ACCOUNT REQUEST
1043// ================================================================================================
1044
1045/// Controls whether vault data is included in a `/GetAccount` response.
1046#[derive(Clone, Debug, Default, Eq, PartialEq)]
1047pub enum VaultFetch {
1048    /// Do not include vault data in the response.
1049    #[default]
1050    Skip,
1051    /// Always include vault data in the response.
1052    Always,
1053    /// Include vault data only if the account's current vault root differs from this commitment.
1054    ///
1055    /// An omitted asset list is byte-identical to a genuinely empty vault, so callers must keep the
1056    /// vault whose root they send and verify any reconstruction against the header's vault root.
1057    IfChangedFrom(Word),
1058    /// Include proofs for these assets only, in [`AccountVaultDetails::Partial`]. The node proves
1059    /// an asset that is not in the vault as absent.
1060    ///
1061    /// The node accepts at most
1062    /// [`RpcLimits::vault_assets_limit`](crate::rpc::RpcLimits::vault_assets_limit) assets in a
1063    /// single request.
1064    Assets(BTreeSet<AssetId>),
1065}
1066
1067impl Serializable for VaultFetch {
1068    fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
1069        match self {
1070            Self::Skip => target.write_u8(0),
1071            Self::Always => target.write_u8(1),
1072            Self::IfChangedFrom(root) => {
1073                target.write_u8(2);
1074                target.write(root);
1075            },
1076            Self::Assets(asset_ids) => {
1077                target.write_u8(3);
1078                target.write(asset_ids);
1079            },
1080        }
1081    }
1082}
1083
1084impl Deserializable for VaultFetch {
1085    fn read_from<R: miden_tx::utils::serde::ByteReader>(
1086        source: &mut R,
1087    ) -> Result<Self, DeserializationError> {
1088        match source.read_u8()? {
1089            0 => Ok(Self::Skip),
1090            1 => Ok(Self::Always),
1091            2 => Ok(Self::IfChangedFrom(source.read()?)),
1092            3 => Ok(Self::Assets(source.read()?)),
1093            tag => Err(DeserializationError::InvalidValue(format!(
1094                "invalid vault requirements tag: {tag}"
1095            ))),
1096        }
1097    }
1098}
1099
1100impl From<VaultFetch> for Option<VaultRequest> {
1101    /// Encodes the policy as the request's `vault_request`: `None` skips the vault, the empty word
1102    /// (which no real vault root equals) always fetches it, a concrete commitment fetches it only
1103    /// when it differs, and a set of asset IDs requests proofs for those assets.
1104    fn from(vault: VaultFetch) -> Self {
1105        let commitment = match vault {
1106            VaultFetch::Skip => return None,
1107            VaultFetch::Always => EMPTY_WORD,
1108            VaultFetch::IfChangedFrom(commitment) => commitment,
1109            VaultFetch::Assets(asset_ids) => {
1110                return Some(VaultRequest::AssetIds(VaultAssetIds {
1111                    asset_ids: asset_ids.into_iter().map(Into::into).collect(),
1112                }));
1113            },
1114        };
1115        Some(VaultRequest::AssetVaultCommitment(commitment.into()))
1116    }
1117}
1118
1119/// Which storage map entries to include in a `/GetAccount` response.
1120///
1121/// Mirrors the node's `AccountDetailRequest` storage request: the storage header (slot roots) is
1122/// always returned; this only controls which map *entries* come with it. The variants are mutually
1123/// exclusive.
1124#[derive(Clone, Debug, Default, Eq, PartialEq)]
1125pub enum StorageMapFetch {
1126    /// Don't request any map entries; only the storage header is returned.
1127    #[default]
1128    Skip,
1129    /// Request entries for every storage map slot, without naming the slots in advance. Oversize
1130    /// maps come back as [`StorageMapEntries::LimitExceeded`], to be resolved via
1131    /// [`crate::rpc::NodeRpcClient::sync_storage_maps`].
1132    All,
1133    /// Request entries only for the explicitly named slots. See [`AccountStorageRequirements`] for
1134    /// the per-slot semantics.
1135    Slots(AccountStorageRequirements),
1136}
1137
1138impl Serializable for StorageMapFetch {
1139    fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
1140        match self {
1141            Self::Skip => target.write_u8(0),
1142            Self::All => target.write_u8(1),
1143            Self::Slots(requirements) => {
1144                target.write_u8(2);
1145                target.write(requirements);
1146            },
1147        }
1148    }
1149}
1150
1151impl Deserializable for StorageMapFetch {
1152    fn read_from<R: miden_tx::utils::serde::ByteReader>(
1153        source: &mut R,
1154    ) -> Result<Self, DeserializationError> {
1155        match source.read_u8()? {
1156            0 => Ok(Self::Skip),
1157            1 => Ok(Self::All),
1158            2 => Ok(Self::Slots(source.read()?)),
1159            tag => Err(DeserializationError::InvalidValue(format!(
1160                "invalid storage map fetch tag: {tag}"
1161            ))),
1162        }
1163    }
1164}
1165
1166impl From<StorageMapFetch> for Option<StorageRequest> {
1167    fn from(storage: StorageMapFetch) -> Self {
1168        match storage {
1169            StorageMapFetch::Skip => None,
1170            StorageMapFetch::All => Some(StorageRequest::AllStorageMaps(true)),
1171            StorageMapFetch::Slots(reqs) => {
1172                Some(StorageRequest::StorageMaps(StorageMapDetailRequests {
1173                    storage_maps: reqs.into(),
1174                }))
1175            },
1176        }
1177    }
1178}
1179
1180/// Storage map entries and vault data to fetch for an account.
1181///
1182/// Both policies default to `Skip`. A transaction reads skipped entries and assets during
1183/// execution, one lookup at a time, from the store or from the node.
1184#[derive(Clone, Debug, Default, Eq, PartialEq)]
1185pub struct AccountFetch {
1186    storage: StorageMapFetch,
1187    vault: VaultFetch,
1188}
1189
1190impl AccountFetch {
1191    /// Creates a fetch policy that skips storage map entries and vault data.
1192    #[must_use]
1193    pub fn new() -> Self {
1194        Self::default()
1195    }
1196
1197    /// Sets which storage map entries to fetch.
1198    #[must_use]
1199    pub fn with_storage(mut self, storage: StorageMapFetch) -> Self {
1200        self.storage = storage;
1201        self
1202    }
1203
1204    /// Sets which vault data to fetch.
1205    #[must_use]
1206    pub fn with_vault(mut self, vault: VaultFetch) -> Self {
1207        self.vault = vault;
1208        self
1209    }
1210
1211    /// Returns the storage map fetch policy.
1212    pub fn storage(&self) -> &StorageMapFetch {
1213        &self.storage
1214    }
1215
1216    /// Returns the vault fetch policy.
1217    pub fn vault(&self) -> &VaultFetch {
1218        &self.vault
1219    }
1220}
1221
1222impl Serializable for AccountFetch {
1223    fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
1224        target.write(&self.storage);
1225        target.write(&self.vault);
1226    }
1227}
1228
1229impl Deserializable for AccountFetch {
1230    fn read_from<R: miden_tx::utils::serde::ByteReader>(
1231        source: &mut R,
1232    ) -> Result<Self, DeserializationError> {
1233        Ok(Self {
1234            storage: source.read()?,
1235            vault: source.read()?,
1236        })
1237    }
1238}
1239
1240/// Parameters for [`crate::rpc::NodeRpcClient::get_account`].
1241///
1242/// The node returns details for public accounts only. For a private account, the response holds the
1243/// witness alone, and `fetch` and `known_code` have no effect.
1244#[derive(Clone, Debug)]
1245pub struct GetAccountRequest {
1246    /// Account to fetch.
1247    pub account_id: AccountId,
1248    /// Storage map entries and vault data to include in the response.
1249    pub fetch: AccountFetch,
1250    /// Block at which to retrieve the proof.
1251    pub at: AccountStateAt,
1252    /// Code the client already has. The node omits code with the same commitment.
1253    pub known_code: Option<AccountCode>,
1254}
1255
1256impl GetAccountRequest {
1257    /// Creates a request at the chain tip with no map entries, known code, or vault data.
1258    #[must_use]
1259    pub fn new(account_id: AccountId) -> Self {
1260        Self {
1261            account_id,
1262            fetch: AccountFetch::default(),
1263            at: AccountStateAt::ChainTip,
1264            known_code: None,
1265        }
1266    }
1267
1268    /// Sets the storage map entries and vault data to include in the response.
1269    #[must_use]
1270    pub fn with_fetch(mut self, fetch: AccountFetch) -> Self {
1271        self.fetch = fetch;
1272        self
1273    }
1274
1275    /// Sets the target block for this request.
1276    #[must_use]
1277    pub fn at(mut self, at: AccountStateAt) -> Self {
1278        self.at = at;
1279        self
1280    }
1281
1282    /// Provides the code commitment the client already holds, so the node can skip re-sending
1283    /// matching code.
1284    #[must_use]
1285    pub fn with_known_code(mut self, known_code: Option<AccountCode>) -> Self {
1286        self.known_code = known_code;
1287        self
1288    }
1289}
1290
1291// ERRORS
1292// ================================================================================================
1293
1294#[derive(Debug, Error)]
1295pub enum AccountProofError {
1296    #[error(
1297        "the received account commitment doesn't match the received account header's commitment"
1298    )]
1299    InconsistentAccountCommitment,
1300    #[error("the received account id doesn't match the received account header's id")]
1301    InconsistentAccountId,
1302    #[error(
1303        "the received code commitment doesn't match the received account header's code commitment"
1304    )]
1305    InconsistentCodeCommitment,
1306}
1307
1308#[cfg(test)]
1309mod tests {
1310    use miden_protocol::Word;
1311    use miden_protocol::account::{StorageMapKey, StorageSlotName};
1312    use miden_protocol::asset::AssetId;
1313    use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
1314    use miden_tx::utils::serde::{Deserializable, Serializable};
1315
1316    use super::{AccountFetch, AccountStorageRequirements, StorageMapFetch, VaultFetch, proto};
1317    use crate::alloc::string::ToString;
1318
1319    #[test]
1320    fn invitation_code_validation_request_debug_hides_code() {
1321        let code = "secret-invitation-code";
1322        let request =
1323            proto::rpc::IsInvitationCodeValidRequest { invitation_code: code.to_string() };
1324
1325        let debug = format!("{request:?}");
1326        assert!(debug.contains("IsInvitationCodeValidRequest"));
1327        assert!(!debug.contains(code));
1328    }
1329
1330    #[test]
1331    fn account_fetch_roundtrip() {
1332        let slot = StorageSlotName::new("miden::testing::map").unwrap();
1333        let key = StorageMapKey::new(Word::from([1u32; 4]));
1334        let storage = AccountStorageRequirements::new([(slot, &[key])]);
1335        let asset_id = AssetId::new_fungible(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap());
1336
1337        for storage in
1338            [StorageMapFetch::Skip, StorageMapFetch::All, StorageMapFetch::Slots(storage)]
1339        {
1340            let fetch = AccountFetch::new().with_storage(storage);
1341            for fetch in [
1342                fetch.clone(),
1343                fetch.clone().with_vault(VaultFetch::Skip),
1344                fetch.clone().with_vault(VaultFetch::Always),
1345                fetch.clone().with_vault(VaultFetch::IfChangedFrom(Word::from([2u32; 4]))),
1346                fetch.with_vault(VaultFetch::Assets([asset_id].into())),
1347            ] {
1348                let decoded = AccountFetch::read_from_bytes(&fetch.to_bytes()).unwrap();
1349                assert_eq!(decoded, fetch);
1350            }
1351        }
1352    }
1353}
1354
1355#[cfg(all(test, feature = "testing", feature = "tonic"))]
1356mod vault_details_tests {
1357    use alloc::collections::BTreeSet;
1358    use alloc::vec::Vec;
1359
1360    use miden_protocol::account::AccountId;
1361    use miden_protocol::asset::{Asset, AssetId, AssetVault, FungibleAsset, PartialVault};
1362    use miden_protocol::testing::account_id::{
1363        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1364        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
1365    };
1366
1367    use super::{AccountVaultDetails, VaultAssetIds, VaultFetch, VaultRequest, proto};
1368
1369    /// Returns a vault that holds one asset, that asset, and the ID of an asset that is not in the
1370    /// vault.
1371    fn vault_with_one_asset() -> (AssetVault, Asset, AssetId) {
1372        let held_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
1373        let absent_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap();
1374        let asset: Asset = FungibleAsset::new(held_faucet, 100).unwrap().into();
1375        let vault = AssetVault::new(&[asset]).unwrap();
1376        (vault, asset, AssetId::new_fungible(absent_faucet))
1377    }
1378
1379    /// Returns the proofs of `asset_ids` in `vault`.
1380    fn partial_vault(vault: &AssetVault, asset_ids: &[AssetId]) -> PartialVault {
1381        PartialVault::with_witnesses(asset_ids.iter().map(|asset_id| vault.open(*asset_id)))
1382            .unwrap()
1383    }
1384
1385    /// Returns vault details that hold the proofs of `asset_ids` in `vault`.
1386    fn proof_details(vault: &AssetVault, asset_ids: &[AssetId]) -> AccountVaultDetails {
1387        AccountVaultDetails::Partial(partial_vault(vault, asset_ids))
1388    }
1389
1390    fn asset_request(asset_ids: &[AssetId]) -> VaultFetch {
1391        VaultFetch::Assets(asset_ids.iter().copied().collect())
1392    }
1393
1394    #[test]
1395    fn asset_request_encodes_the_asset_ids() {
1396        let (_, asset, absent) = vault_with_one_asset();
1397        let asset_ids = BTreeSet::from([asset.id(), absent]);
1398
1399        let request: Option<VaultRequest> = VaultFetch::Assets(asset_ids.clone()).into();
1400
1401        let expected = asset_ids.into_iter().map(Into::into).collect();
1402        assert_eq!(request, Some(VaultRequest::AssetIds(VaultAssetIds { asset_ids: expected })));
1403    }
1404
1405    #[test]
1406    fn proofs_that_cover_the_request_are_accepted() {
1407        let (vault, asset, absent) = vault_with_one_asset();
1408        let request = asset_request(&[asset.id(), absent]);
1409
1410        let details = proof_details(&vault, &[asset.id(), absent])
1411            .validate_against_request(vault.root(), &request)
1412            .unwrap();
1413
1414        let partial_vault = details.into_partial_vault().expect("expected vault proofs");
1415        assert_eq!(partial_vault.get(asset.id()).unwrap(), Some(asset));
1416        assert_eq!(partial_vault.get(absent).unwrap(), None);
1417    }
1418
1419    #[test]
1420    fn proofs_against_another_root_are_rejected() {
1421        let (vault, asset, _) = vault_with_one_asset();
1422        let other_root = AssetVault::default().root();
1423
1424        let result = proof_details(&vault, &[asset.id()])
1425            .validate_against_request(other_root, &asset_request(&[asset.id()]));
1426
1427        assert!(result.is_err());
1428    }
1429
1430    /// The vault holds a second asset, so the proof of the first asset does not cover it. With one
1431    /// asset only, its proof would also prove the absence of every other asset.
1432    #[test]
1433    fn proofs_that_miss_a_requested_asset_are_rejected() {
1434        let (_, asset, _) = vault_with_one_asset();
1435        let other_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap();
1436        let other_asset: Asset = FungibleAsset::new(other_faucet, 5).unwrap().into();
1437        let vault = AssetVault::new(&[asset, other_asset]).unwrap();
1438
1439        let result = proof_details(&vault, &[asset.id()]).validate_against_request(
1440            vault.root(),
1441            &asset_request(&[asset.id(), other_asset.id()]),
1442        );
1443
1444        assert!(result.is_err());
1445    }
1446
1447    #[test]
1448    fn proofs_that_were_not_requested_are_rejected() {
1449        let (vault, asset, _) = vault_with_one_asset();
1450
1451        let result = proof_details(&vault, &[asset.id()])
1452            .validate_against_request(vault.root(), &VaultFetch::Always);
1453
1454        assert!(result.is_err());
1455    }
1456
1457    /// The node can send the full vault contents instead of proofs. The client converts that answer
1458    /// into proofs, so the callers handle only one shape.
1459    #[test]
1460    fn full_vault_answer_is_converted_to_proofs() {
1461        let (vault, asset, absent) = vault_with_one_asset();
1462        let details = AccountVaultDetails::Assets(vec![asset])
1463            .validate_against_request(vault.root(), &asset_request(&[asset.id(), absent]))
1464            .unwrap();
1465
1466        let partial_vault = details.into_partial_vault().expect("expected vault proofs");
1467        assert_eq!(partial_vault.get(asset.id()).unwrap(), Some(asset));
1468        assert_eq!(partial_vault.get(absent).unwrap(), None);
1469    }
1470
1471    #[test]
1472    fn too_many_assets_answer_to_an_asset_request_is_rejected() {
1473        let (vault, asset, _) = vault_with_one_asset();
1474        let result = AccountVaultDetails::TooManyAssets
1475            .validate_against_request(vault.root(), &asset_request(&[asset.id()]));
1476
1477        assert!(result.is_err());
1478    }
1479
1480    #[test]
1481    fn partial_vault_round_trips_through_proto() {
1482        let (vault, asset, absent) = vault_with_one_asset();
1483        let expected = partial_vault(&vault, &[asset.id(), absent]);
1484        let response = proto::rpc::AccountVaultDetails {
1485            too_many_assets: false,
1486            assets: Vec::new(),
1487            partial_vault: Some(proto::rpc::account_vault_details::PartialVault {
1488                asset_ids: vec![asset.id().into(), absent.into()],
1489                partial_smt: Some(expected.partial_smt().clone().into()),
1490            }),
1491        };
1492
1493        let details = AccountVaultDetails::try_from(response).unwrap();
1494
1495        assert_eq!(details.into_partial_vault(), Some(expected));
1496    }
1497
1498    #[test]
1499    fn partial_vault_with_an_asset_list_is_rejected() {
1500        let (vault, asset, _) = vault_with_one_asset();
1501        let partial_vault = partial_vault(&vault, &[asset.id()]);
1502        let response = proto::rpc::AccountVaultDetails {
1503            too_many_assets: false,
1504            assets: vec![asset.into()],
1505            partial_vault: Some(proto::rpc::account_vault_details::PartialVault {
1506                asset_ids: vec![asset.id().into()],
1507                partial_smt: Some(partial_vault.partial_smt().clone().into()),
1508            }),
1509        };
1510
1511        assert!(AccountVaultDetails::try_from(response).is_err());
1512    }
1513
1514    #[test]
1515    fn too_many_assets_flag_converts_to_its_variant() {
1516        let response = proto::rpc::AccountVaultDetails {
1517            too_many_assets: true,
1518            assets: Vec::new(),
1519            partial_vault: None,
1520        };
1521
1522        let details = AccountVaultDetails::try_from(response).unwrap();
1523
1524        assert!(details.is_too_many_assets());
1525    }
1526
1527    #[test]
1528    fn too_many_assets_flag_with_an_asset_list_is_rejected() {
1529        let (_, asset, _) = vault_with_one_asset();
1530        let response = proto::rpc::AccountVaultDetails {
1531            too_many_assets: true,
1532            assets: vec![asset.into()],
1533            partial_vault: None,
1534        };
1535
1536        assert!(AccountVaultDetails::try_from(response).is_err());
1537    }
1538
1539    #[test]
1540    fn partial_vault_with_the_too_many_assets_flag_is_rejected() {
1541        let (vault, asset, _) = vault_with_one_asset();
1542        let partial_vault = partial_vault(&vault, &[asset.id()]);
1543        let response = proto::rpc::AccountVaultDetails {
1544            too_many_assets: true,
1545            assets: Vec::new(),
1546            partial_vault: Some(proto::rpc::account_vault_details::PartialVault {
1547                asset_ids: vec![asset.id().into()],
1548                partial_smt: Some(partial_vault.partial_smt().clone().into()),
1549            }),
1550        };
1551
1552        assert!(AccountVaultDetails::try_from(response).is_err());
1553    }
1554
1555    #[test]
1556    fn proofs_that_track_an_asset_that_was_not_requested_are_rejected() {
1557        let (_, asset, _) = vault_with_one_asset();
1558        let other_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap();
1559        let other_asset: Asset = FungibleAsset::new(other_faucet, 5).unwrap().into();
1560        let vault = AssetVault::new(&[asset, other_asset]).unwrap();
1561
1562        let result = proof_details(&vault, &[asset.id(), other_asset.id()])
1563            .validate_against_request(vault.root(), &asset_request(&[asset.id()]));
1564
1565        assert!(result.is_err());
1566    }
1567}