Skip to main content

miden_client/rpc/domain/
account.rs

1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3use miden_protocol::account::{
4    Account, AccountCode, AccountHeader, AccountId, AccountStorage, AccountStorageHeader,
5    StorageMap, StorageMapKey, StorageSlot, StorageSlotName, StorageSlotType,
6};
7use miden_protocol::asset::{Asset, AssetVault};
8use miden_protocol::block::BlockNumber;
9use miden_protocol::block::account_tree::AccountWitness;
10use miden_protocol::crypto::merkle::SparseMerklePath;
11use miden_protocol::crypto::merkle::smt::PartialSmt;
12use miden_objects::DecodeMessageExt;
13use miden_protocol::{EMPTY_WORD, Word};
14use miden_tx::utils::serde::{Deserializable, Serializable};
15use thiserror::Error;
16
17use crate::alloc::string::ToString;
18use crate::rpc::{AccountStateAt, RpcError};
19use crate::rpc::domain::MissingFieldHelper;
20use crate::rpc::generated::rpc::account_request::account_detail_request::storage_map_detail_request::{MapKeys, SlotData};
21use crate::rpc::generated::rpc::account_request::account_detail_request::{
22    StorageMapDetailRequest, StorageMapDetailRequests, StorageRequest,
23};
24use crate::rpc::generated::{self as proto};
25
26// FROM PROTO ACCOUNT HEADERS
27// ================================================================================================
28
29#[cfg(feature = "tonic")]
30impl proto::rpc::account_response::AccountDetails {
31    /// Converts the RPC response into `AccountDetails`.
32    ///
33    /// The RPC response may omit unchanged account codes. If so, this function uses
34    /// `known_account_codes` to fill in the missing code. If a required code cannot be found in the
35    /// response or `known_account_codes`, an error is returned.
36    ///
37    /// `storage_requirements` is the request this response answers, used to check that each partial
38    /// map covers exactly the keys that were asked for.
39    ///
40    /// # Errors
41    /// - If account code is missing both on `self` and `known_account_codes`
42    /// - If data cannot be correctly deserialized
43    /// - If a partial map does not cover exactly the keys requested for its slot
44    pub fn into_domain(
45        self,
46        known_account_codes: &BTreeMap<Word, AccountCode>,
47        storage_requirements: &AccountStorageRequirements,
48    ) -> Result<AccountDetails, crate::rpc::RpcError> {
49        use crate::rpc::RpcError;
50        use crate::rpc::domain::MissingFieldHelper;
51
52        let proto::rpc::account_response::AccountDetails {
53            header,
54            storage_details,
55            code,
56            vault_details,
57        } = self;
58        let header: AccountHeader = header
59            .ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!(header)))?
60            .decode_and_verify()?;
61
62        let storage_details: AccountStorageDetails = storage_details
63            .ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!(
64                storage_details
65            )))?
66            .try_into()?;
67
68        storage_details.validate_against_request(storage_requirements)?;
69
70        // If an account code was received, it means the previously known account code is no longer
71        // valid. If it was not, it means we sent a code commitment that matched and so our code is
72        // still valid
73        let code = {
74            let received_code: Option<AccountCode> =
75                code.map(DecodeMessageExt::decode_and_verify).transpose()?;
76            match received_code {
77                Some(code) => code,
78                None => known_account_codes
79                    .get(&header.code_commitment())
80                    .ok_or(RpcError::InvalidResponse(
81                        "Account code was not provided, but the response did not contain it either"
82                            .into(),
83                    ))?
84                    .clone(),
85            }
86        };
87
88        let vault_details = vault_details
89            .ok_or(proto::rpc::AccountVaultDetails::missing_field(stringify!(vault_details)))?
90            .try_into()?;
91
92        Ok(AccountDetails {
93            header,
94            storage_details,
95            code,
96            vault_details,
97        })
98    }
99}
100
101// ACCOUNT PROOF
102// ================================================================================================
103
104/// Contains a block number, and a list of account proofs at that block.
105pub type AccountProofs = (BlockNumber, Vec<AccountProof>);
106
107// ACCOUNT DETAILS
108// ================================================================================================
109
110/// An account details.
111#[derive(Clone, Debug)]
112pub struct AccountDetails {
113    pub header: AccountHeader,
114    pub storage_details: AccountStorageDetails,
115    pub code: AccountCode,
116    pub vault_details: AccountVaultDetails,
117}
118
119impl TryFrom<&AccountDetails> for Account {
120    type Error = RpcError;
121
122    /// Builds an [`Account`] from [`AccountDetails`].
123    ///
124    /// This conversion fails if the account details are incomplete, i.e., when the account's
125    /// storage maps or vault exceed the node's size threshold, or when only specific map keys were
126    /// requested.
127    fn try_from(details: &AccountDetails) -> Result<Self, Self::Error> {
128        if details.vault_details.too_many_assets {
129            return Err(RpcError::ExpectedDataMissing(
130                "cannot build account: vault has too many assets".into(),
131            ));
132        }
133
134        if let Some(slot_name) = details
135            .storage_details
136            .map_details
137            .iter()
138            .find(|m| m.is_limit_exceeded())
139            .map(|m| &m.slot_name)
140        {
141            return Err(RpcError::ExpectedDataMissing(format!(
142                "cannot build account: storage map slot '{slot_name}' has too many entries",
143            )));
144        }
145
146        let mut slots: Vec<StorageSlot> = Vec::new();
147
148        for slot_header in details.storage_details.header.slots() {
149            match slot_header.slot_type() {
150                StorageSlotType::Value => {
151                    slots.push(StorageSlot::with_value(
152                        slot_header.name().clone(),
153                        slot_header.value(),
154                    ));
155                },
156                StorageSlotType::Map => {
157                    let map_details = details
158                        .storage_details
159                        .find_map_details(slot_header.name())
160                        .ok_or_else(|| {
161                            RpcError::ExpectedDataMissing(format!(
162                                "slot '{}' is a map but has no map_details in response",
163                                slot_header.name()
164                            ))
165                        })?;
166
167                    let storage_map = map_details
168                        .entries
169                        .clone()
170                        .into_storage_map()
171                        .ok_or_else(|| {
172                            RpcError::ExpectedDataMissing(format!(
173                                "slot '{}' did not come back with all its entries, so the full \
174                                 account cannot be built",
175                                slot_header.name(),
176                            ))
177                        })?
178                        .map_err(|err| {
179                            RpcError::InvalidResponse(format!(
180                                "the rpc api returned a non-valid map entry: {err}"
181                            ))
182                        })?;
183
184                    slots.push(StorageSlot::with_map(slot_header.name().clone(), storage_map));
185                },
186            }
187        }
188
189        let asset_vault = AssetVault::new(&details.vault_details.assets).map_err(|err| {
190            RpcError::InvalidResponse(format!("rpc api returned non-valid assets: {err}"))
191        })?;
192
193        let account_storage = AccountStorage::new(slots).map_err(|err| {
194            RpcError::InvalidResponse(format!("rpc api returned non-valid storage slots: {err}"))
195        })?;
196
197        Account::new(
198            details.header.id(),
199            asset_vault,
200            account_storage,
201            details.code.clone(),
202            details.header.nonce(),
203            None,
204        )
205        .map_err(|err| {
206            RpcError::InvalidResponse(format!(
207                "failed to construct account from rpc api response: {err}"
208            ))
209        })
210    }
211}
212
213// ACCOUNT STORAGE DETAILS
214// ================================================================================================
215
216/// Account storage details for `AccountResponse`
217#[derive(Clone, Debug)]
218pub struct AccountStorageDetails {
219    /// Account storage header (storage slot info for up to 256 slots)
220    pub header: AccountStorageHeader,
221    /// Additional data for the requested storage maps
222    pub map_details: Vec<AccountStorageMapDetails>,
223}
224
225impl AccountStorageDetails {
226    /// Find the matching details for a map, given its storage slot name.
227    //  This linear search should be good enough since there can be
228    //  only up to 256 slots, so locality probably wins here.
229    pub fn find_map_details(&self, target: &StorageSlotName) -> Option<&AccountStorageMapDetails> {
230        self.map_details.iter().find(|map_detail| map_detail.slot_name == *target)
231    }
232
233    /// Checks that every partial map covers exactly the keys that were requested for its slot.
234    ///
235    /// # Errors
236    /// - If a partial map does not cover a key that was requested for its slot.
237    /// - If a partial map covers a key that was not requested.
238    pub fn validate_against_request(
239        &self,
240        storage_requirements: &AccountStorageRequirements,
241    ) -> Result<(), RpcError> {
242        for map_detail in &self.map_details {
243            let StorageMapEntries::PartialMap { map_keys, .. } = &map_detail.entries else {
244                continue;
245            };
246
247            let requested_keys = storage_requirements.keys_for_slot(&map_detail.slot_name);
248            if let Some(key) = requested_keys.iter().find(|key| !map_keys.contains(key)) {
249                return Err(RpcError::InvalidResponse(format!(
250                    "partial storage map for slot '{}' does not cover requested key {}",
251                    map_detail.slot_name,
252                    key.to_hex(),
253                )));
254            }
255            if let Some(key) = map_keys.iter().find(|key| !requested_keys.contains(key)) {
256                return Err(RpcError::InvalidResponse(format!(
257                    "partial storage map for slot '{}' covers key {}, which was not requested",
258                    map_detail.slot_name,
259                    key.to_hex(),
260                )));
261            }
262        }
263
264        Ok(())
265    }
266}
267
268impl TryFrom<proto::rpc::AccountStorageDetails> for AccountStorageDetails {
269    type Error = RpcError;
270
271    fn try_from(value: proto::rpc::AccountStorageDetails) -> Result<Self, Self::Error> {
272        let header: AccountStorageHeader = value
273            .header
274            .ok_or(proto::account::AccountStorageHeader::missing_field(stringify!(header)))?
275            .decode_and_verify()?;
276        let map_details = value
277            .map_details
278            .into_iter()
279            .map(core::convert::TryInto::try_into)
280            .collect::<Result<Vec<AccountStorageMapDetails>, RpcError>>()?;
281
282        // A partial map is only worth anything if it is anchored to the slot root the account
283        // commitment covers. Without this check the node could serve a self-consistent tree of its
284        // own making.
285        for map_detail in &map_details {
286            let StorageMapEntries::PartialMap { partial_smt, .. } = &map_detail.entries else {
287                continue;
288            };
289
290            let slot = header
291                .slots()
292                .find(|slot| *slot.name() == map_detail.slot_name)
293                .ok_or_else(|| {
294                    RpcError::InvalidResponse(format!(
295                        "partial storage map references slot '{}', which is absent from the \
296                         storage header",
297                        map_detail.slot_name,
298                    ))
299                })?;
300            if slot.slot_type() != StorageSlotType::Map {
301                return Err(RpcError::InvalidResponse(format!(
302                    "partial storage map references slot '{}', which is not a map",
303                    map_detail.slot_name,
304                )));
305            }
306            if partial_smt.root() != slot.value() {
307                return Err(RpcError::InvalidResponse(format!(
308                    "partial storage map for slot '{}' has root {} but the storage header reports \
309                     {}",
310                    map_detail.slot_name,
311                    partial_smt.root(),
312                    slot.value(),
313                )));
314            }
315        }
316
317        Ok(Self { header, map_details })
318    }
319}
320
321// ACCOUNT MAP DETAILS
322// ================================================================================================
323
324#[derive(Clone, Debug)]
325pub struct AccountStorageMapDetails {
326    /// Storage slot name of the storage map.
327    pub slot_name: StorageSlotName,
328    /// The map data the node returned for this slot. The variants are mutually exclusive.
329    pub entries: StorageMapEntries,
330}
331
332impl AccountStorageMapDetails {
333    /// The maximum number of keys the node will cover with a single partial map. The node counts
334    /// this across all slots of a request, so honouring it per slot is a conservative bound.
335    pub const MAX_PARTIAL_MAP_KEYS: usize = 64;
336
337    /// Returns `true` when the node reported that this slot has more entries than it will return in
338    /// a single response, meaning the entries have to be fetched through
339    /// [`crate::rpc::NodeRpcClient::sync_storage_maps`] instead.
340    pub fn is_limit_exceeded(&self) -> bool {
341        matches!(self.entries, StorageMapEntries::LimitExceeded)
342    }
343}
344
345impl TryFrom<proto::rpc::account_storage_details::AccountStorageMapDetails>
346    for AccountStorageMapDetails
347{
348    type Error = RpcError;
349
350    fn try_from(
351        value: proto::rpc::account_storage_details::AccountStorageMapDetails,
352    ) -> Result<Self, Self::Error> {
353        use proto::rpc::account_storage_details::account_storage_map_details::Result as ProtoResult;
354
355        let slot_name = StorageSlotName::new(value.slot_name)
356            .map_err(|err| RpcError::ExpectedDataMissing(err.to_string()))?;
357
358        let entries = match value.result {
359            Some(ProtoResult::TooManyEntries(true)) => StorageMapEntries::LimitExceeded,
360            Some(ProtoResult::TooManyEntries(false)) => {
361                return Err(RpcError::InvalidResponse(
362                    "too_many_entries must be true when set".into(),
363                ));
364            },
365            Some(ProtoResult::AllEntries(all_entries)) => {
366                let entries = all_entries
367                    .entries
368                    .into_iter()
369                    .map(core::convert::TryInto::try_into)
370                    .collect::<Result<Vec<StorageMapEntry>, RpcError>>()?;
371                StorageMapEntries::AllEntries(entries)
372            },
373            Some(ProtoResult::PartialMap(partial_map)) => {
374                if partial_map.map_keys.len() > Self::MAX_PARTIAL_MAP_KEYS {
375                    return Err(RpcError::InvalidResponse(format!(
376                        "partial storage map for slot '{slot_name}' contains {} keys, exceeding \
377                         the limit of {}",
378                        partial_map.map_keys.len(),
379                        Self::MAX_PARTIAL_MAP_KEYS,
380                    )));
381                }
382
383                let map_keys = partial_map
384                    .map_keys
385                    .into_iter()
386                    .map(|key| Word::try_from(key).map(StorageMapKey::new))
387                    .collect::<Result<Vec<_>, _>>()?;
388                if let Some(key) = first_duplicate_key(&map_keys) {
389                    return Err(RpcError::InvalidResponse(format!(
390                        "partial storage map for slot '{slot_name}' repeats key {}",
391                        key.to_hex(),
392                    )));
393                }
394
395                let partial_smt: PartialSmt = partial_map
396                        .partial_smt
397                        .ok_or(proto::rpc::account_storage_details::account_storage_map_details::PartialStorageMap::missing_field(
398                            stringify!(partial_smt),
399                        ))?
400                        .decode_and_verify()?;
401
402                // The response sends the values only inside the tree, so a key the tree does not
403                // track carries no value at all and would fail later, at read time.
404                for key in &map_keys {
405                    partial_smt.get_value(&key.hash().as_word()).map_err(|_| {
406                        RpcError::InvalidResponse(format!(
407                            "partial storage map for slot '{slot_name}' does not track key {}",
408                            key.to_hex(),
409                        ))
410                    })?;
411                }
412
413                StorageMapEntries::PartialMap { map_keys, partial_smt }
414            },
415            None => {
416                return Err(RpcError::InvalidResponse(format!(
417                    "storage map details for slot '{slot_name}' carry no result",
418                )));
419            },
420        };
421
422        Ok(Self { slot_name, entries })
423    }
424}
425
426/// Returns the first key that appears more than once, if any.
427///
428/// The key lists this guards are bounded by [`AccountStorageMapDetails::MAX_PARTIAL_MAP_KEYS`], so
429/// the quadratic scan avoids allocating a set.
430fn first_duplicate_key(keys: &[StorageMapKey]) -> Option<&StorageMapKey> {
431    keys.iter()
432        .enumerate()
433        .find_map(|(index, key)| keys[..index].contains(key).then_some(key))
434}
435
436// STORAGE MAP ENTRY
437// ================================================================================================
438
439/// A storage map entry containing a key-value pair.
440#[derive(Clone, Debug)]
441pub struct StorageMapEntry {
442    pub key: StorageMapKey,
443    pub value: Word,
444}
445
446impl TryFrom<proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry>
447    for StorageMapEntry
448{
449    type Error = RpcError;
450
451    fn try_from(value: proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry) -> Result<Self, Self::Error> {
452        let key = Word::try_from(value.key.ok_or(RpcError::ExpectedDataMissing("key".into()))?)
453            .map(StorageMapKey::new)?;
454        let value = value.value.ok_or(RpcError::ExpectedDataMissing("value".into()))?.try_into()?;
455        Ok(Self { key, value })
456    }
457}
458
459// STORAGE MAP ENTRIES
460// ================================================================================================
461
462/// The map data a `/GetAccount` response carries for one storage map slot. The variants are
463/// mutually exclusive, mirroring the node's response.
464#[derive(Clone, Debug)]
465pub enum StorageMapEntries {
466    /// The slot has more entries than the node returns in a single response. No entries are
467    /// carried; fetch them with [`crate::rpc::NodeRpcClient::sync_storage_maps`].
468    LimitExceeded,
469    /// All entries in the storage map (no proofs needed as the full map is available).
470    AllEntries(Vec<StorageMapEntry>),
471    /// The specific keys that were requested, covered by a single partial SMT.
472    ///
473    /// The values are carried only inside `partial_smt`: read one by hashing its raw key and
474    /// calling [`PartialSmt::get_value`]. Every key in `map_keys` is guaranteed to be tracked by
475    /// `partial_smt`, so such a read cannot fail.
476    PartialMap {
477        /// The original, unhashed keys covered by `partial_smt`.
478        map_keys: Vec<StorageMapKey>,
479        /// The partial SMT proving the value of every key in `map_keys`.
480        partial_smt: PartialSmt,
481    },
482}
483
484impl StorageMapEntries {
485    /// Converts the entries into a [`StorageMap`].
486    ///
487    /// Returns `None` for every variant other than [`AllEntries`](Self::AllEntries), since only
488    /// that one carries the whole map.
489    pub fn into_storage_map(
490        self,
491    ) -> Option<Result<StorageMap, miden_protocol::errors::StorageMapError>> {
492        match self {
493            StorageMapEntries::AllEntries(entries) => {
494                Some(StorageMap::with_entries(entries.into_iter().map(|e| (e.key, e.value))))
495            },
496            StorageMapEntries::LimitExceeded | StorageMapEntries::PartialMap { .. } => None,
497        }
498    }
499}
500
501// ACCOUNT VAULT DETAILS
502// ================================================================================================
503
504#[derive(Clone, Debug)]
505pub struct AccountVaultDetails {
506    /// A flag that is set to true if the account contains too many assets. This indicates to the
507    /// user that `SyncAccountVault` endpoint should be used to retrieve the account's assets
508    pub too_many_assets: bool,
509    /// When `too_many_assets` == false, this will contain the list of assets in the account's vault
510    pub assets: Vec<Asset>,
511}
512
513impl TryFrom<proto::rpc::AccountVaultDetails> for AccountVaultDetails {
514    type Error = RpcError;
515
516    fn try_from(value: proto::rpc::AccountVaultDetails) -> Result<Self, Self::Error> {
517        let too_many_assets = value.too_many_assets;
518        let assets = value
519            .assets
520            .into_iter()
521            .map(DecodeMessageExt::decode_and_verify)
522            .collect::<Result<Vec<Asset>, _>>()?;
523
524        Ok(Self { too_many_assets, assets })
525    }
526}
527
528// ACCOUNT PROOF
529// ================================================================================================
530
531/// Represents a proof of existence of an account's state at a specific block number.
532#[derive(Clone, Debug)]
533pub struct AccountProof {
534    /// Account witness.
535    account_witness: AccountWitness,
536    /// State headers of public accounts.
537    state_headers: Option<AccountDetails>,
538}
539
540impl AccountProof {
541    /// Creates a new [`AccountProof`].
542    pub fn new(
543        account_witness: AccountWitness,
544        account_details: Option<AccountDetails>,
545    ) -> Result<Self, AccountProofError> {
546        if let Some(AccountDetails {
547            header: account_header,
548            storage_details: _,
549            code,
550            ..
551        }) = &account_details
552        {
553            if account_header.to_commitment() != account_witness.state_commitment() {
554                return Err(AccountProofError::InconsistentAccountCommitment);
555            }
556            if account_header.id() != account_witness.id() {
557                return Err(AccountProofError::InconsistentAccountId);
558            }
559            if code.commitment() != account_header.code_commitment() {
560                return Err(AccountProofError::InconsistentCodeCommitment);
561            }
562        }
563
564        Ok(Self {
565            account_witness,
566            state_headers: account_details,
567        })
568    }
569
570    /// Returns the account ID related to the account proof.
571    pub fn account_id(&self) -> AccountId {
572        self.account_witness.id()
573    }
574
575    /// Returns the account header, if present.
576    pub fn account_header(&self) -> Option<&AccountHeader> {
577        self.state_headers.as_ref().map(|account_details| &account_details.header)
578    }
579
580    /// Returns the storage header, if present.
581    pub fn storage_header(&self) -> Option<&AccountStorageHeader> {
582        self.state_headers
583            .as_ref()
584            .map(|account_details| &account_details.storage_details.header)
585    }
586
587    /// Returns the full storage details, if available (public accounts only).
588    pub fn storage_details(&self) -> Option<&AccountStorageDetails> {
589        self.state_headers.as_ref().map(|d| &d.storage_details)
590    }
591
592    /// Returns the vault details, if available (public accounts only).
593    pub fn vault_details(&self) -> Option<&AccountVaultDetails> {
594        self.state_headers.as_ref().map(|d| &d.vault_details)
595    }
596
597    /// Returns the storage map details for a specific slot, if available.
598    pub fn find_map_details(
599        &self,
600        slot_name: &StorageSlotName,
601    ) -> Option<&AccountStorageMapDetails> {
602        self.state_headers
603            .as_ref()
604            .and_then(|details| details.storage_details.find_map_details(slot_name))
605    }
606
607    /// Returns the account code, if present.
608    pub fn account_code(&self) -> Option<&AccountCode> {
609        self.state_headers.as_ref().map(|headers| &headers.code)
610    }
611
612    /// Returns the code commitment, if account code is present in the state headers.
613    pub fn code_commitment(&self) -> Option<Word> {
614        self.account_code().map(AccountCode::commitment)
615    }
616
617    /// Returns the current state commitment of the account.
618    pub fn account_commitment(&self) -> Word {
619        self.account_witness.state_commitment()
620    }
621
622    pub fn account_witness(&self) -> &AccountWitness {
623        &self.account_witness
624    }
625
626    /// Returns the proof of the account's inclusion.
627    pub fn merkle_proof(&self) -> &SparseMerklePath {
628        self.account_witness.path()
629    }
630
631    /// Deconstructs `AccountProof` into its individual parts.
632    pub fn into_parts(self) -> (AccountWitness, Option<AccountDetails>) {
633        (self.account_witness, self.state_headers)
634    }
635
636    /// Consumes the proof and returns the account details, if present (public accounts only).
637    pub fn into_details(self) -> Option<AccountDetails> {
638        self.state_headers
639    }
640
641    /// Mutable accessor for the account details, when present.
642    ///
643    /// Useful for resolving oversized vault or storage data in place via
644    /// [`crate::rpc::NodeRpcClient::resolve_oversize_vault`] and
645    /// [`crate::rpc::NodeRpcClient::resolve_oversize_storage_maps`].
646    pub fn details_mut(&mut self) -> Option<&mut AccountDetails> {
647        self.state_headers.as_mut()
648    }
649}
650
651#[cfg(feature = "tonic")]
652impl TryFrom<proto::rpc::AccountResponse> for AccountProof {
653    type Error = RpcError;
654    fn try_from(account_proof: proto::rpc::AccountResponse) -> Result<Self, Self::Error> {
655        let Some(witness) = account_proof.witness else {
656            return Err(RpcError::ExpectedDataMissing(
657                "GetAccount returned an account without witness".to_string(),
658            ));
659        };
660
661        let details: Option<AccountDetails> = {
662            match account_proof.details {
663                None => None,
664                Some(details) => Some(
665                    details
666                        .into_domain(&BTreeMap::new(), &AccountStorageRequirements::default())?,
667                ),
668            }
669        };
670        AccountProof::new(witness.decode_and_verify()?, details)
671            .map_err(|err| RpcError::InvalidResponse(format!("{err}")))
672    }
673}
674
675// ACCOUNT STORAGE REQUEST
676// ================================================================================================
677
678/// Per-slot map data to include in a `/GetAccount` response. Slots absent here are omitted from
679/// `map_details` (the storage header still lists every slot).
680///
681/// - Empty key list: all entries, no proof. May come back as [`StorageMapEntries::LimitExceeded`].
682/// - Non-empty key list: just those keys, covered by one partial SMT.
683#[derive(Clone, Debug, Default, Eq, PartialEq)]
684pub struct AccountStorageRequirements(BTreeMap<StorageSlotName, Vec<StorageMapKey>>);
685
686impl AccountStorageRequirements {
687    /// Requests the specified ke ys per slot, covered by one partial SMT per slot. An empty key
688    /// iterator for a slot behaves like [`Self::all_entries`].
689    ///
690    /// Repeated keys within a slot are collapsed, since the node rejects a request that names the
691    /// same key twice.
692    pub fn new<'a>(
693        slots_and_keys: impl IntoIterator<
694            Item = (StorageSlotName, impl IntoIterator<Item = &'a StorageMapKey>),
695        >,
696    ) -> Self {
697        let map = slots_and_keys
698            .into_iter()
699            .map(|(slot_name, keys_iter)| {
700                let mut keys_vec: Vec<StorageMapKey> = Vec::new();
701                for key in keys_iter {
702                    if !keys_vec.contains(key) {
703                        keys_vec.push(*key);
704                    }
705                }
706                (slot_name, keys_vec)
707            })
708            .collect();
709
710        AccountStorageRequirements(map)
711    }
712
713    /// Requests every entry of each given slot, without a proof. Oversize maps come back as
714    /// [`StorageMapEntries::LimitExceeded`].
715    pub fn all_entries(slot_names: &[StorageSlotName]) -> Self {
716        AccountStorageRequirements(
717            slot_names.iter().map(|name| (name.clone(), Vec::new())).collect(),
718        )
719    }
720
721    pub fn inner(&self) -> &BTreeMap<StorageSlotName, Vec<StorageMapKey>> {
722        &self.0
723    }
724
725    /// Returns the keys requested for a given slot, or an empty slice if none were specified.
726    pub fn keys_for_slot(&self, slot_name: &StorageSlotName) -> &[StorageMapKey] {
727        self.0.get(slot_name).map_or(&[], Vec::as_slice)
728    }
729}
730
731impl From<AccountStorageRequirements> for Vec<StorageMapDetailRequest> {
732    fn from(value: AccountStorageRequirements) -> Vec<StorageMapDetailRequest> {
733        let request_map = value.0;
734        let mut requests = Vec::with_capacity(request_map.len());
735        for (slot_name, map_keys) in request_map {
736            let slot_data = if map_keys.is_empty() {
737                Some(SlotData::AllEntries(true))
738            } else {
739                let keys = map_keys.into_iter().map(|key| Word::from(key).into()).collect();
740                Some(SlotData::MapKeys(MapKeys { map_keys: keys }))
741            };
742            requests.push(StorageMapDetailRequest {
743                slot_name: slot_name.to_string(),
744                slot_data,
745            });
746        }
747        requests
748    }
749}
750
751impl Serializable for AccountStorageRequirements {
752    fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
753        target.write(&self.0);
754    }
755}
756
757impl Deserializable for AccountStorageRequirements {
758    fn read_from<R: miden_tx::utils::serde::ByteReader>(
759        source: &mut R,
760    ) -> Result<Self, miden_tx::utils::serde::DeserializationError> {
761        Ok(AccountStorageRequirements(source.read()?))
762    }
763}
764
765// GET ACCOUNT REQUEST
766// ================================================================================================
767
768/// Controls whether vault data is included in a `/GetAccount` response.
769#[derive(Clone, Debug, Default)]
770pub enum VaultFetch {
771    /// Do not include vault data in the response.
772    #[default]
773    Skip,
774    /// Always include vault data in the response.
775    Always,
776    /// Include vault data only if the account's current vault root differs from this commitment.
777    ///
778    /// An omitted asset list is byte-identical to a genuinely empty vault, so callers must keep the
779    /// vault whose root they send and verify any reconstruction against the header's vault root.
780    IfChangedFrom(Word),
781}
782
783impl From<VaultFetch> for Option<proto::primitives::Word> {
784    /// Encodes the policy as the request's `asset_vault_commitment`: `None` skips the vault, the
785    /// empty word (which no real vault root equals) always fetches it, and a concrete commitment
786    /// fetches only when it differs.
787    fn from(vault: VaultFetch) -> Self {
788        match vault {
789            VaultFetch::Skip => None,
790            VaultFetch::Always => Some(EMPTY_WORD.into()),
791            VaultFetch::IfChangedFrom(commitment) => Some(commitment.into()),
792        }
793    }
794}
795
796/// Which storage map entries to include in a `/GetAccount` response.
797///
798/// Mirrors the node's `AccountDetailRequest` storage request: the storage header (slot roots) is
799/// always returned; this only controls which map *entries* come with it. The variants are mutually
800/// exclusive.
801#[derive(Clone, Debug, Default)]
802pub enum StorageMapFetch {
803    /// Don't request any map entries; only the storage header is returned.
804    #[default]
805    Skip,
806    /// Request entries for every storage map slot, without naming the slots in advance. Oversize
807    /// maps come back as [`StorageMapEntries::LimitExceeded`], to be resolved via
808    /// [`crate::rpc::NodeRpcClient::sync_storage_maps`].
809    All,
810    /// Request entries only for the explicitly named slots. See [`AccountStorageRequirements`] for
811    /// the per-slot semantics.
812    Slots(AccountStorageRequirements),
813}
814
815impl From<StorageMapFetch> for Option<StorageRequest> {
816    fn from(storage: StorageMapFetch) -> Self {
817        match storage {
818            StorageMapFetch::Skip => None,
819            StorageMapFetch::All => Some(StorageRequest::AllStorageMaps(true)),
820            StorageMapFetch::Slots(reqs) => {
821                Some(StorageRequest::StorageMaps(StorageMapDetailRequests {
822                    storage_maps: reqs.into(),
823                }))
824            },
825        }
826    }
827}
828
829/// Parameters for [`crate::rpc::NodeRpcClient::get_account`].
830#[derive(Clone, Debug, Default)]
831pub struct GetAccountRequest {
832    /// Which storage map entries to include in the response.
833    pub storage: StorageMapFetch,
834    /// Block at which to retrieve the proof.
835    pub at: AccountStateAt,
836    /// Code commitment the client already has. When the on-chain commitment matches, the node skips
837    /// re-sending the code.
838    pub known_code: Option<AccountCode>,
839    /// Vault data retrieval policy.
840    pub vault: VaultFetch,
841}
842
843impl GetAccountRequest {
844    /// Creates a request for the minimal account data: the account commitment and storage header at
845    /// the chain tip, with no map entries, no known code, and no vault data. Opt into additional
846    /// data with the builder methods.
847    #[must_use]
848    pub fn new() -> Self {
849        Self {
850            storage: StorageMapFetch::Skip,
851            at: AccountStateAt::ChainTip,
852            known_code: None,
853            vault: VaultFetch::Skip,
854        }
855    }
856
857    /// Sets which storage map entries to include in the response.
858    #[must_use]
859    pub fn with_storage(mut self, storage: StorageMapFetch) -> Self {
860        self.storage = storage;
861        self
862    }
863
864    /// Sets the target block for this request.
865    #[must_use]
866    pub fn at(mut self, at: AccountStateAt) -> Self {
867        self.at = at;
868        self
869    }
870
871    /// Provides the code commitment the client already holds, so the node can skip re-sending
872    /// matching code.
873    #[must_use]
874    pub fn with_known_code(mut self, known_code: Option<AccountCode>) -> Self {
875        self.known_code = known_code;
876        self
877    }
878
879    /// Sets the vault data retrieval policy.
880    #[must_use]
881    pub fn with_vault(mut self, vault: VaultFetch) -> Self {
882        self.vault = vault;
883        self
884    }
885}
886
887// ERRORS
888// ================================================================================================
889
890#[derive(Debug, Error)]
891pub enum AccountProofError {
892    #[error(
893        "the received account commitment doesn't match the received account header's commitment"
894    )]
895    InconsistentAccountCommitment,
896    #[error("the received account id doesn't match the received account header's id")]
897    InconsistentAccountId,
898    #[error(
899        "the received code commitment doesn't match the received account header's code commitment"
900    )]
901    InconsistentCodeCommitment,
902}