Skip to main content

miden_client/rpc/domain/
account.rs

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