Skip to main content

miden_client/rpc/domain/
account.rs

1use alloc::boxed::Box;
2use alloc::collections::BTreeMap;
3use alloc::vec::Vec;
4use core::fmt::{self, Debug, Display, Formatter};
5
6use miden_protocol::Word;
7use miden_protocol::account::{
8    Account, AccountCode, AccountHeader, AccountId, AccountStorage, AccountStorageHeader,
9    StorageMap, StorageMapKey, StorageSlot, StorageSlotHeader, StorageSlotName, StorageSlotType,
10};
11use miden_protocol::asset::{Asset, AssetVault};
12use miden_protocol::block::BlockNumber;
13use miden_protocol::block::account_tree::AccountWitness;
14use miden_protocol::crypto::merkle::SparseMerklePath;
15use miden_protocol::crypto::merkle::smt::SmtProof;
16use miden_tx::utils::ToHex;
17use miden_tx::utils::serde::{Deserializable, Serializable};
18use thiserror::Error;
19
20use crate::alloc::string::ToString;
21use crate::rpc::RpcError;
22use crate::rpc::domain::MissingFieldHelper;
23use crate::rpc::errors::RpcConversionError;
24use crate::rpc::generated::rpc::account_request::account_detail_request::storage_map_detail_request::{MapKeys, SlotData};
25use crate::rpc::generated::rpc::account_request::account_detail_request::StorageMapDetailRequest;
26use crate::rpc::generated::{self as proto};
27
28// FETCHED ACCOUNT
29// ================================================================================================
30
31/// Describes the possible responses from the `GetAccountDetails` endpoint for an account.
32#[derive(Debug)]
33pub enum FetchedAccount {
34    /// Private accounts are stored off-chain. Only a commitment to the state of the account is
35    /// shared with the network. The full account state is to be tracked locally.
36    Private(AccountId, AccountUpdateSummary),
37    /// Public accounts are recorded on-chain. As such, its state is shared with the network and
38    /// can always be retrieved through the appropriate RPC method.
39    Public(Box<Account>, AccountUpdateSummary),
40}
41
42impl FetchedAccount {
43    /// Creates a [`FetchedAccount`] corresponding to a private account tracked only by its ID and
44    /// update summary.
45    pub fn new_private(account_id: AccountId, summary: AccountUpdateSummary) -> Self {
46        Self::Private(account_id, summary)
47    }
48
49    /// Creates a [`FetchedAccount`] for a public account with its full [`Account`] state.
50    pub fn new_public(account: Account, summary: AccountUpdateSummary) -> Self {
51        Self::Public(Box::new(account), summary)
52    }
53
54    /// Returns the account ID.
55    pub fn account_id(&self) -> AccountId {
56        match self {
57            Self::Private(account_id, _) => *account_id,
58            Self::Public(account, _) => account.id(),
59        }
60    }
61
62    // Returns the account update summary commitment
63    pub fn commitment(&self) -> Word {
64        match self {
65            Self::Private(_, summary) | Self::Public(_, summary) => summary.commitment,
66        }
67    }
68
69    // Returns the associated account if the account is public, otherwise none
70    pub fn account(&self) -> Option<&Account> {
71        match self {
72            Self::Private(..) => None,
73            Self::Public(account, _) => Some(account.as_ref()),
74        }
75    }
76}
77
78impl From<FetchedAccount> for Option<Account> {
79    fn from(acc: FetchedAccount) -> Self {
80        match acc {
81            FetchedAccount::Private(..) => None,
82            FetchedAccount::Public(account, _) => Some(*account),
83        }
84    }
85}
86
87// ACCOUNT UPDATE SUMMARY
88// ================================================================================================
89
90/// Contains public updated information about the account requested.
91#[derive(Debug)]
92pub struct AccountUpdateSummary {
93    /// Commitment of the account, that represents a commitment to its updated state.
94    pub commitment: Word,
95    /// Block number of last account update.
96    pub last_block_num: BlockNumber,
97}
98
99impl AccountUpdateSummary {
100    /// Creates a new [`AccountUpdateSummary`].
101    pub fn new(commitment: Word, last_block_num: BlockNumber) -> Self {
102        Self { commitment, last_block_num }
103    }
104}
105
106// ACCOUNT ID
107// ================================================================================================
108
109impl Display for proto::account::AccountId {
110    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
111        f.write_fmt(format_args!("0x{}", self.id.to_hex()))
112    }
113}
114
115impl Debug for proto::account::AccountId {
116    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
117        Display::fmt(self, f)
118    }
119}
120
121// INTO PROTO ACCOUNT ID
122// ================================================================================================
123
124impl From<AccountId> for proto::account::AccountId {
125    fn from(account_id: AccountId) -> Self {
126        Self { id: account_id.to_bytes() }
127    }
128}
129
130// FROM PROTO ACCOUNT ID
131// ================================================================================================
132
133impl TryFrom<proto::account::AccountId> for AccountId {
134    type Error = RpcConversionError;
135
136    fn try_from(account_id: proto::account::AccountId) -> Result<Self, Self::Error> {
137        AccountId::read_from_bytes(&account_id.id).map_err(|_| RpcConversionError::NotAValidFelt)
138    }
139}
140
141// ACCOUNT HEADER
142// ================================================================================================
143
144impl TryInto<AccountHeader> for proto::account::AccountHeader {
145    type Error = crate::rpc::RpcError;
146
147    fn try_into(self) -> Result<AccountHeader, Self::Error> {
148        use miden_protocol::Felt;
149
150        use crate::rpc::domain::MissingFieldHelper;
151
152        let proto::account::AccountHeader {
153            account_id,
154            nonce,
155            vault_root,
156            storage_commitment,
157            code_commitment,
158        } = self;
159
160        let account_id: AccountId = account_id
161            .ok_or(proto::account::AccountHeader::missing_field(stringify!(account_id)))?
162            .try_into()?;
163        let vault_root = vault_root
164            .ok_or(proto::account::AccountHeader::missing_field(stringify!(vault_root)))?
165            .try_into()?;
166        let storage_commitment = storage_commitment
167            .ok_or(proto::account::AccountHeader::missing_field(stringify!(storage_commitment)))?
168            .try_into()?;
169        let code_commitment = code_commitment
170            .ok_or(proto::account::AccountHeader::missing_field(stringify!(code_commitment)))?
171            .try_into()?;
172
173        Ok(AccountHeader::new(
174            account_id,
175            Felt::new(nonce),
176            vault_root,
177            storage_commitment,
178            code_commitment,
179        ))
180    }
181}
182
183// ACCOUNT STORAGE HEADER
184// ================================================================================================
185
186impl TryInto<AccountStorageHeader> for proto::account::AccountStorageHeader {
187    type Error = crate::rpc::RpcError;
188
189    fn try_into(self) -> Result<AccountStorageHeader, Self::Error> {
190        use crate::rpc::RpcError;
191        use crate::rpc::domain::MissingFieldHelper;
192
193        let mut header_slots: Vec<StorageSlotHeader> = Vec::with_capacity(self.slots.len());
194
195        for slot in self.slots {
196            let slot_value: Word = slot
197                .commitment
198                .ok_or(proto::account::account_storage_header::StorageSlot::missing_field(
199                    stringify!(commitment),
200                ))?
201                .try_into()?;
202
203            let slot_type = u8::try_from(slot.slot_type)
204                .map_err(|e| RpcError::InvalidResponse(e.to_string()))
205                .and_then(|v| {
206                    StorageSlotType::try_from(v)
207                        .map_err(|e| RpcError::InvalidResponse(e.to_string()))
208                })?;
209            let slot_name = StorageSlotName::new(slot.slot_name)
210                .map_err(|err| RpcError::InvalidResponse(err.to_string()))?;
211
212            header_slots.push(StorageSlotHeader::new(slot_name, slot_type, slot_value));
213        }
214
215        header_slots.sort_by_key(StorageSlotHeader::id);
216        AccountStorageHeader::new(header_slots)
217            .map_err(|err| RpcError::InvalidResponse(err.to_string()))
218    }
219}
220
221// FROM PROTO ACCOUNT HEADERS
222// ================================================================================================
223
224#[cfg(feature = "tonic")]
225impl proto::rpc::account_response::AccountDetails {
226    /// Converts the RPC response into `AccountDetails`.
227    ///
228    /// The RPC response may omit unchanged account codes. If so, this function uses
229    /// `known_account_codes` to fill in the missing code. If a required code cannot be found in
230    /// the response or `known_account_codes`, an error is returned.
231    ///
232    /// # Errors
233    /// - If account code is missing both on `self` and `known_account_codes`
234    /// - If data cannot be correctly deserialized
235    pub fn into_domain(
236        self,
237        known_account_codes: &BTreeMap<Word, AccountCode>,
238        storage_requirements: &AccountStorageRequirements,
239    ) -> Result<AccountDetails, crate::rpc::RpcError> {
240        use crate::rpc::RpcError;
241        use crate::rpc::domain::MissingFieldHelper;
242
243        let proto::rpc::account_response::AccountDetails {
244            header,
245            storage_details,
246            code,
247            vault_details,
248        } = self;
249        let header: AccountHeader = header
250            .ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!(header)))?
251            .try_into()?;
252
253        let storage_details: AccountStorageDetails = storage_details
254            .ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!(
255                storage_details
256            )))?
257            .try_into()?;
258
259        // Validate that the returned proofs match the originally requested keys.
260        // The node returns hashed SMT keys, so we hash the raw keys and check
261        // they are present in the corresponding proofs.
262        for map_detail in &storage_details.map_details {
263            let requested_keys = storage_requirements
264                .inner()
265                .get(&map_detail.slot_name)
266                .map(Vec::as_slice)
267                .unwrap_or_default();
268
269            if let StorageMapEntries::EntriesWithProofs(proofs) = &map_detail.entries {
270                if proofs.len() != requested_keys.len() {
271                    return Err(RpcError::InvalidResponse(format!(
272                        "expected {} proofs for storage map slot '{}', got {}",
273                        requested_keys.len(),
274                        map_detail.slot_name,
275                        proofs.len(),
276                    )));
277                }
278                for (proof, raw_key) in proofs.iter().zip(requested_keys.iter()) {
279                    let hashed_key = raw_key.hash().as_word();
280                    if proof.get(&hashed_key).is_none() {
281                        return Err(RpcError::InvalidResponse(format!(
282                            "proof for storage map key {} does not match the requested key",
283                            raw_key.to_hex(),
284                        )));
285                    }
286                }
287            }
288        }
289
290        // If an account code was received, it means the previously known account code is no longer
291        // valid. If it was not, it means we sent a code commitment that matched and so our code
292        // is still valid
293        let code = {
294            let received_code = code.map(|c| AccountCode::read_from_bytes(&c)).transpose()?;
295            match received_code {
296                Some(code) => code,
297                None => known_account_codes
298                    .get(&header.code_commitment())
299                    .ok_or(RpcError::InvalidResponse(
300                        "Account code was not provided, but the response did not contain it either"
301                            .into(),
302                    ))?
303                    .clone(),
304            }
305        };
306
307        let vault_details = vault_details
308            .ok_or(proto::rpc::AccountVaultDetails::missing_field(stringify!(vault_details)))?
309            .try_into()?;
310
311        Ok(AccountDetails {
312            header,
313            storage_details,
314            code,
315            vault_details,
316        })
317    }
318}
319
320// ACCOUNT PROOF
321// ================================================================================================
322
323/// Contains a block number, and a list of account proofs at that block.
324pub type AccountProofs = (BlockNumber, Vec<AccountProof>);
325
326// ACCOUNT DETAILS
327// ================================================================================================
328
329/// An account details.
330#[derive(Clone, Debug)]
331pub struct AccountDetails {
332    pub header: AccountHeader,
333    pub storage_details: AccountStorageDetails,
334    pub code: AccountCode,
335    pub vault_details: AccountVaultDetails,
336}
337
338impl TryFrom<&AccountDetails> for Account {
339    type Error = RpcError;
340
341    /// Builds an [`Account`] from [`AccountDetails`].
342    ///
343    /// This conversion fails if the account details are incomplete, i.e., when the account's
344    /// storage maps or vault exceed the node's size threshold (`too_many_entries` or
345    /// `too_many_assets` flags are set).
346    fn try_from(details: &AccountDetails) -> Result<Self, Self::Error> {
347        if details.vault_details.too_many_assets {
348            return Err(RpcError::ExpectedDataMissing(
349                "cannot build account: vault has too many assets".into(),
350            ));
351        }
352
353        if let Some(slot_name) = details
354            .storage_details
355            .map_details
356            .iter()
357            .find(|m| m.too_many_entries)
358            .map(|m| &m.slot_name)
359        {
360            return Err(RpcError::ExpectedDataMissing(format!(
361                "cannot build account: storage map slot '{slot_name}' has too many entries",
362            )));
363        }
364
365        let mut slots: Vec<StorageSlot> = Vec::new();
366
367        for slot_header in details.storage_details.header.slots() {
368            match slot_header.slot_type() {
369                StorageSlotType::Value => {
370                    slots.push(StorageSlot::with_value(
371                        slot_header.name().clone(),
372                        slot_header.value(),
373                    ));
374                },
375                StorageSlotType::Map => {
376                    let map_details = details
377                        .storage_details
378                        .find_map_details(slot_header.name())
379                        .ok_or_else(|| {
380                            RpcError::ExpectedDataMissing(format!(
381                                "slot '{}' is a map but has no map_details in response",
382                                slot_header.name()
383                            ))
384                        })?;
385
386                    let storage_map = map_details
387                        .entries
388                        .clone()
389                        .into_storage_map()
390                        .ok_or_else(|| {
391                            RpcError::ExpectedDataMissing(
392                                "expected AllEntries for full account fetch, got EntriesWithProofs"
393                                    .into(),
394                            )
395                        })?
396                        .map_err(|err| {
397                            RpcError::InvalidResponse(format!(
398                                "the rpc api returned a non-valid map entry: {err}"
399                            ))
400                        })?;
401
402                    slots.push(StorageSlot::with_map(slot_header.name().clone(), storage_map));
403                },
404            }
405        }
406
407        let asset_vault = AssetVault::new(&details.vault_details.assets).map_err(|err| {
408            RpcError::InvalidResponse(format!("rpc api returned non-valid assets: {err}"))
409        })?;
410
411        let account_storage = AccountStorage::new(slots).map_err(|err| {
412            RpcError::InvalidResponse(format!("rpc api returned non-valid storage slots: {err}"))
413        })?;
414
415        Account::new(
416            details.header.id(),
417            asset_vault,
418            account_storage,
419            details.code.clone(),
420            details.header.nonce(),
421            None,
422        )
423        .map_err(|err| {
424            RpcError::InvalidResponse(format!(
425                "failed to construct account from rpc api response: {err}"
426            ))
427        })
428    }
429}
430
431// ACCOUNT STORAGE DETAILS
432// ================================================================================================
433
434/// Account storage details for `AccountResponse`
435#[derive(Clone, Debug)]
436pub struct AccountStorageDetails {
437    /// Account storage header (storage slot info for up to 256 slots)
438    pub header: AccountStorageHeader,
439    /// Additional data for the requested storage maps
440    pub map_details: Vec<AccountStorageMapDetails>,
441}
442
443impl AccountStorageDetails {
444    /// Find the matching details for a map, given its storage slot name.
445    //  This linear search should be good enough since there can be
446    //  only up to 256 slots, so locality probably wins here.
447    pub fn find_map_details(&self, target: &StorageSlotName) -> Option<&AccountStorageMapDetails> {
448        self.map_details.iter().find(|map_detail| map_detail.slot_name == *target)
449    }
450}
451
452impl TryFrom<proto::rpc::AccountStorageDetails> for AccountStorageDetails {
453    type Error = RpcError;
454
455    fn try_from(value: proto::rpc::AccountStorageDetails) -> Result<Self, Self::Error> {
456        let header = value
457            .header
458            .ok_or(proto::account::AccountStorageHeader::missing_field(stringify!(header)))?
459            .try_into()?;
460        let map_details = value
461            .map_details
462            .into_iter()
463            .map(core::convert::TryInto::try_into)
464            .collect::<Result<Vec<AccountStorageMapDetails>, RpcError>>()?;
465
466        Ok(Self { header, map_details })
467    }
468}
469
470// ACCOUNT MAP DETAILS
471// ================================================================================================
472
473#[derive(Clone, Debug)]
474pub struct AccountStorageMapDetails {
475    /// Storage slot name of the storage map.
476    pub slot_name: StorageSlotName,
477    /// A flag that is set to `true` if the number of to-be-returned entries in the
478    /// storage map would exceed a threshold. This indicates to the user that `SyncStorageMaps`
479    /// endpoint should be used to get all storage map data.
480    pub too_many_entries: bool,
481    /// Storage map entries - either all entries (for small/full maps) or entries with proofs
482    /// (for partial maps).
483    pub entries: StorageMapEntries,
484}
485
486impl TryFrom<proto::rpc::account_storage_details::AccountStorageMapDetails>
487    for AccountStorageMapDetails
488{
489    type Error = RpcError;
490
491    fn try_from(
492        value: proto::rpc::account_storage_details::AccountStorageMapDetails,
493    ) -> Result<Self, Self::Error> {
494        use proto::rpc::account_storage_details::account_storage_map_details::Entries;
495
496        let slot_name = StorageSlotName::new(value.slot_name)
497            .map_err(|err| RpcError::ExpectedDataMissing(err.to_string()))?;
498        let too_many_entries = value.too_many_entries;
499
500        let entries = match value.entries {
501            Some(Entries::AllEntries(all_entries)) => {
502                let entries = all_entries
503                    .entries
504                    .into_iter()
505                    .map(core::convert::TryInto::try_into)
506                    .collect::<Result<Vec<StorageMapEntry>, RpcError>>()?;
507                StorageMapEntries::AllEntries(entries)
508            },
509            Some(Entries::EntriesWithProofs(entries_with_proofs)) => {
510                let proofs = entries_with_proofs
511                    .entries
512                    .into_iter()
513                    .map(|entry| {
514                        let proof: SmtProof = entry
515                            .proof
516                            .ok_or(RpcError::ExpectedDataMissing("proof".into()))?
517                            .try_into()?;
518                        Ok(proof)
519                    })
520                    .collect::<Result<Vec<SmtProof>, RpcError>>()?;
521                StorageMapEntries::EntriesWithProofs(proofs)
522            },
523            None => StorageMapEntries::AllEntries(Vec::new()),
524        };
525
526        Ok(Self { slot_name, too_many_entries, entries })
527    }
528}
529
530// STORAGE MAP ENTRY
531// ================================================================================================
532
533/// A storage map entry containing a key-value pair.
534#[derive(Clone, Debug)]
535pub struct StorageMapEntry {
536    pub key: StorageMapKey,
537    pub value: Word,
538}
539
540impl TryFrom<proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry>
541    for StorageMapEntry
542{
543    type Error = RpcError;
544
545    fn try_from(value: proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry) -> Result<Self, Self::Error> {
546        let key: StorageMapKey =
547            value.key.ok_or(RpcError::ExpectedDataMissing("key".into()))?.try_into()?;
548        let value = value.value.ok_or(RpcError::ExpectedDataMissing("value".into()))?.try_into()?;
549        Ok(Self { key, value })
550    }
551}
552
553// STORAGE MAP ENTRIES
554// ================================================================================================
555
556/// Storage map entries, either all entries (for small/full maps) or raw SMT proofs
557/// (for specific key queries).
558#[derive(Clone, Debug)]
559pub enum StorageMapEntries {
560    /// All entries in the storage map (no proofs needed as the full map is available).
561    AllEntries(Vec<StorageMapEntry>),
562    /// Specific entries with their SMT proofs (for partial maps).
563    EntriesWithProofs(Vec<SmtProof>),
564}
565
566impl StorageMapEntries {
567    /// Converts the entries into a [`StorageMap`].
568    ///
569    /// Returns `None` for the [`EntriesWithProofs`](Self::EntriesWithProofs) variant because it
570    /// contains partial data (SMT proofs) that cannot produce a complete [`StorageMap`].
571    pub fn into_storage_map(
572        self,
573    ) -> Option<Result<StorageMap, miden_protocol::errors::StorageMapError>> {
574        match self {
575            StorageMapEntries::AllEntries(entries) => {
576                Some(StorageMap::with_entries(entries.into_iter().map(|e| (e.key, e.value))))
577            },
578            StorageMapEntries::EntriesWithProofs(_) => None,
579        }
580    }
581}
582
583// ACCOUNT VAULT DETAILS
584// ================================================================================================
585
586#[derive(Clone, Debug)]
587pub struct AccountVaultDetails {
588    /// A flag that is set to true if the account contains too many assets. This indicates
589    /// to the user that `SyncAccountVault` endpoint should be used to retrieve the
590    /// account's assets
591    pub too_many_assets: bool,
592    /// When `too_many_assets` == false, this will contain the list of assets in the
593    /// account's vault
594    pub assets: Vec<Asset>,
595}
596
597impl TryFrom<proto::rpc::AccountVaultDetails> for AccountVaultDetails {
598    type Error = RpcError;
599
600    fn try_from(value: proto::rpc::AccountVaultDetails) -> Result<Self, Self::Error> {
601        let too_many_assets = value.too_many_assets;
602        let assets = value
603            .assets
604            .into_iter()
605            .map(Asset::try_from)
606            .collect::<Result<Vec<Asset>, _>>()?;
607
608        Ok(Self { too_many_assets, assets })
609    }
610}
611
612// ACCOUNT PROOF
613// ================================================================================================
614
615/// Represents a proof of existence of an account's state at a specific block number.
616#[derive(Clone, Debug)]
617pub struct AccountProof {
618    /// Account witness.
619    account_witness: AccountWitness,
620    /// State headers of public accounts.
621    state_headers: Option<AccountDetails>,
622}
623
624impl AccountProof {
625    /// Creates a new [`AccountProof`].
626    pub fn new(
627        account_witness: AccountWitness,
628        account_details: Option<AccountDetails>,
629    ) -> Result<Self, AccountProofError> {
630        if let Some(AccountDetails {
631            header: account_header,
632            storage_details: _,
633            code,
634            ..
635        }) = &account_details
636        {
637            if account_header.to_commitment() != account_witness.state_commitment() {
638                return Err(AccountProofError::InconsistentAccountCommitment);
639            }
640            if account_header.id() != account_witness.id() {
641                return Err(AccountProofError::InconsistentAccountId);
642            }
643            if code.commitment() != account_header.code_commitment() {
644                return Err(AccountProofError::InconsistentCodeCommitment);
645            }
646        }
647
648        Ok(Self {
649            account_witness,
650            state_headers: account_details,
651        })
652    }
653
654    /// Returns the account ID related to the account proof.
655    pub fn account_id(&self) -> AccountId {
656        self.account_witness.id()
657    }
658
659    /// Returns the account header, if present.
660    pub fn account_header(&self) -> Option<&AccountHeader> {
661        self.state_headers.as_ref().map(|account_details| &account_details.header)
662    }
663
664    /// Returns the storage header, if present.
665    pub fn storage_header(&self) -> Option<&AccountStorageHeader> {
666        self.state_headers
667            .as_ref()
668            .map(|account_details| &account_details.storage_details.header)
669    }
670
671    /// Returns the full storage details, if available (public accounts only).
672    pub fn storage_details(&self) -> Option<&AccountStorageDetails> {
673        self.state_headers.as_ref().map(|d| &d.storage_details)
674    }
675
676    /// Returns the storage map details for a specific slot, if available.
677    pub fn find_map_details(
678        &self,
679        slot_name: &StorageSlotName,
680    ) -> Option<&AccountStorageMapDetails> {
681        self.state_headers
682            .as_ref()
683            .and_then(|details| details.storage_details.find_map_details(slot_name))
684    }
685
686    /// Returns the account code, if present.
687    pub fn account_code(&self) -> Option<&AccountCode> {
688        self.state_headers.as_ref().map(|headers| &headers.code)
689    }
690
691    /// Returns the code commitment, if account code is present in the state headers.
692    pub fn code_commitment(&self) -> Option<Word> {
693        self.account_code().map(AccountCode::commitment)
694    }
695
696    /// Returns the current state commitment of the account.
697    pub fn account_commitment(&self) -> Word {
698        self.account_witness.state_commitment()
699    }
700
701    pub fn account_witness(&self) -> &AccountWitness {
702        &self.account_witness
703    }
704
705    /// Returns the proof of the account's inclusion.
706    pub fn merkle_proof(&self) -> &SparseMerklePath {
707        self.account_witness.path()
708    }
709
710    /// Deconstructs `AccountProof` into its individual parts.
711    pub fn into_parts(self) -> (AccountWitness, Option<AccountDetails>) {
712        (self.account_witness, self.state_headers)
713    }
714}
715
716#[cfg(feature = "tonic")]
717impl TryFrom<proto::rpc::AccountResponse> for AccountProof {
718    type Error = RpcError;
719    fn try_from(account_proof: proto::rpc::AccountResponse) -> Result<Self, Self::Error> {
720        let Some(witness) = account_proof.witness else {
721            return Err(RpcError::ExpectedDataMissing(
722                "GetAccountProof returned an account without witness".to_string(),
723            ));
724        };
725
726        let details: Option<AccountDetails> = {
727            match account_proof.details {
728                None => None,
729                Some(details) => Some(
730                    details
731                        .into_domain(&BTreeMap::new(), &AccountStorageRequirements::default())?,
732                ),
733            }
734        };
735        AccountProof::new(witness.try_into()?, details)
736            .map_err(|err| RpcError::InvalidResponse(format!("{err}")))
737    }
738}
739
740// ACCOUNT WITNESS
741// ================================================================================================
742
743impl TryFrom<proto::account::AccountWitness> for AccountWitness {
744    type Error = RpcError;
745
746    fn try_from(account_witness: proto::account::AccountWitness) -> Result<Self, Self::Error> {
747        let state_commitment = account_witness
748            .commitment
749            .ok_or(proto::account::AccountWitness::missing_field(stringify!(state_commitment)))?
750            .try_into()?;
751        let merkle_path = account_witness
752            .path
753            .ok_or(proto::account::AccountWitness::missing_field(stringify!(merkle_path)))?
754            .try_into()?;
755        let account_id = account_witness
756            .witness_id
757            .ok_or(proto::account::AccountWitness::missing_field(stringify!(witness_id)))?
758            .try_into()?;
759
760        let witness = AccountWitness::new(account_id, state_commitment, merkle_path).unwrap();
761        Ok(witness)
762    }
763}
764
765// ACCOUNT STORAGE REQUEST
766// ================================================================================================
767
768/// Describes storage slots indices to be requested, as well as a list of keys for each of those
769/// slots.
770///
771/// Note: If no specific keys are provided for a slot, all entries are requested. Though the
772/// node may respond with `too_many_entries` if the map exceeds the response limit.
773#[derive(Clone, Debug, Default, Eq, PartialEq)]
774pub struct AccountStorageRequirements(BTreeMap<StorageSlotName, Vec<StorageMapKey>>);
775
776impl AccountStorageRequirements {
777    pub fn new<'a>(
778        slots_and_keys: impl IntoIterator<
779            Item = (StorageSlotName, impl IntoIterator<Item = &'a StorageMapKey>),
780        >,
781    ) -> Self {
782        let map = slots_and_keys
783            .into_iter()
784            .map(|(slot_name, keys_iter)| {
785                let keys_vec: Vec<StorageMapKey> = keys_iter.into_iter().copied().collect();
786                (slot_name, keys_vec)
787            })
788            .collect();
789
790        AccountStorageRequirements(map)
791    }
792
793    pub fn inner(&self) -> &BTreeMap<StorageSlotName, Vec<StorageMapKey>> {
794        &self.0
795    }
796
797    /// Returns the keys requested for a given slot, or an empty slice if none were specified.
798    pub fn keys_for_slot(&self, slot_name: &StorageSlotName) -> &[StorageMapKey] {
799        self.0.get(slot_name).map_or(&[], Vec::as_slice)
800    }
801}
802
803impl From<AccountStorageRequirements> for Vec<StorageMapDetailRequest> {
804    fn from(value: AccountStorageRequirements) -> Vec<StorageMapDetailRequest> {
805        let request_map = value.0;
806        let mut requests = Vec::with_capacity(request_map.len());
807        for (slot_name, map_keys) in request_map {
808            let slot_data = if map_keys.is_empty() {
809                Some(SlotData::AllEntries(true))
810            } else {
811                let keys = map_keys.into_iter().map(|key| Word::from(key).into()).collect();
812                Some(SlotData::MapKeys(MapKeys { map_keys: keys }))
813            };
814            requests.push(StorageMapDetailRequest {
815                slot_name: slot_name.to_string(),
816                slot_data,
817            });
818        }
819        requests
820    }
821}
822
823impl Serializable for AccountStorageRequirements {
824    fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
825        target.write(&self.0);
826    }
827}
828
829impl Deserializable for AccountStorageRequirements {
830    fn read_from<R: miden_tx::utils::serde::ByteReader>(
831        source: &mut R,
832    ) -> Result<Self, miden_tx::utils::serde::DeserializationError> {
833        Ok(AccountStorageRequirements(source.read()?))
834    }
835}
836
837// ERRORS
838// ================================================================================================
839
840#[derive(Debug, Error)]
841pub enum AccountProofError {
842    #[error(
843        "the received account commitment doesn't match the received account header's commitment"
844    )]
845    InconsistentAccountCommitment,
846    #[error("the received account id doesn't match the received account header's id")]
847    InconsistentAccountId,
848    #[error(
849        "the received code commitment doesn't match the received account header's code commitment"
850    )]
851    InconsistentCodeCommitment,
852}