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