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