Skip to main content

miden_client/rpc/domain/
account.rs

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