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_objects::Word;
7use miden_objects::account::{
8    Account,
9    AccountCode,
10    AccountHeader,
11    AccountId,
12    AccountStorageHeader,
13    StorageSlotType,
14};
15use miden_objects::asset::Asset;
16use miden_objects::block::{AccountWitness, BlockNumber};
17use miden_objects::crypto::merkle::SparseMerklePath;
18use miden_tx::utils::{Deserializable, Serializable, ToHex};
19use thiserror::Error;
20
21use crate::alloc::borrow::ToOwned;
22use crate::alloc::string::ToString;
23use crate::rpc::RpcError;
24use crate::rpc::domain::MissingFieldHelper;
25use crate::rpc::errors::RpcConversionError;
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.
32pub enum FetchedAccount {
33    /// Private accounts are stored off-chain. Only a commitment to the state of the account is
34    /// shared with the network. The full account state is to be tracked locally.
35    Private(AccountId, AccountUpdateSummary),
36    /// Public accounts are recorded on-chain. As such, its state is shared with the network and
37    /// can always be retrieved through the appropriate RPC method.
38    Public(Box<Account>, AccountUpdateSummary),
39}
40
41impl FetchedAccount {
42    /// Creates a [`FetchedAccount`] corresponding to a private account tracked only by its ID and
43    /// update summary.
44    pub fn new_private(account_id: AccountId, summary: AccountUpdateSummary) -> Self {
45        Self::Private(account_id, summary)
46    }
47
48    /// Creates a [`FetchedAccount`] for a public account with its full [`Account`] state.
49    pub fn new_public(account: Account, summary: AccountUpdateSummary) -> Self {
50        Self::Public(Box::new(account), summary)
51    }
52
53    /// Returns the account ID.
54    pub fn account_id(&self) -> AccountId {
55        match self {
56            Self::Private(account_id, _) => *account_id,
57            Self::Public(account, _) => account.id(),
58        }
59    }
60
61    // Returns the account update summary commitment
62    pub fn commitment(&self) -> Word {
63        match self {
64            Self::Private(_, summary) | Self::Public(_, summary) => summary.commitment,
65        }
66    }
67
68    // Returns the associated account if the account is public, otherwise none
69    pub fn account(&self) -> Option<&Account> {
70        match self {
71            Self::Private(..) => None,
72            Self::Public(account, _) => Some(account.as_ref()),
73        }
74    }
75}
76
77impl From<FetchedAccount> for Option<Account> {
78    fn from(acc: FetchedAccount) -> Self {
79        match acc {
80            FetchedAccount::Private(..) => None,
81            FetchedAccount::Public(account, _) => Some(*account),
82        }
83    }
84}
85
86// ACCOUNT UPDATE SUMMARY
87// ================================================================================================
88
89/// Contains public updated information about the account requested.
90pub struct AccountUpdateSummary {
91    /// Commitment of the account, that represents a commitment to its updated state.
92    pub commitment: Word,
93    /// Block number of last account update.
94    pub last_block_num: u32,
95}
96
97impl AccountUpdateSummary {
98    /// Creates a new [`AccountUpdateSummary`].
99    pub fn new(commitment: Word, last_block_num: u32) -> Self {
100        Self { commitment, last_block_num }
101    }
102}
103
104// ACCOUNT ID
105// ================================================================================================
106
107impl Display for proto::account::AccountId {
108    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
109        f.write_fmt(format_args!("0x{}", self.id.to_hex()))
110    }
111}
112
113impl Debug for proto::account::AccountId {
114    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
115        Display::fmt(self, f)
116    }
117}
118
119// INTO PROTO ACCOUNT ID
120// ================================================================================================
121
122impl From<AccountId> for proto::account::AccountId {
123    fn from(account_id: AccountId) -> Self {
124        Self { id: account_id.to_bytes() }
125    }
126}
127
128// FROM PROTO ACCOUNT ID
129// ================================================================================================
130
131impl TryFrom<proto::account::AccountId> for AccountId {
132    type Error = RpcConversionError;
133
134    fn try_from(account_id: proto::account::AccountId) -> Result<Self, Self::Error> {
135        AccountId::read_from_bytes(&account_id.id).map_err(|_| RpcConversionError::NotAValidFelt)
136    }
137}
138
139// ACCOUNT HEADER
140// ================================================================================================
141
142pub(crate) struct SlotTypeProto(pub u32);
143
144impl TryInto<StorageSlotType> for SlotTypeProto {
145    type Error = crate::rpc::RpcError;
146
147    fn try_into(self) -> Result<StorageSlotType, Self::Error> {
148        match self.0 {
149            0 => Ok(StorageSlotType::Map),
150            1 => Ok(StorageSlotType::Value),
151            _ => Err(RpcError::InvalidResponse("Invalid storage slot type".into())),
152        }
153    }
154}
155
156impl TryInto<AccountHeader> for proto::account::AccountHeader {
157    type Error = crate::rpc::RpcError;
158
159    fn try_into(self) -> Result<AccountHeader, Self::Error> {
160        use miden_objects::Felt;
161
162        use crate::rpc::domain::MissingFieldHelper;
163
164        let proto::account::AccountHeader {
165            account_id,
166            nonce,
167            vault_root,
168            storage_commitment,
169            code_commitment,
170        } = self;
171
172        let account_id: AccountId = account_id
173            .ok_or(proto::account::AccountHeader::missing_field(stringify!(account_id)))?
174            .try_into()?;
175        let vault_root = vault_root
176            .ok_or(proto::account::AccountHeader::missing_field(stringify!(vault_root)))?
177            .try_into()?;
178        let storage_commitment = storage_commitment
179            .ok_or(proto::account::AccountHeader::missing_field(stringify!(storage_commitment)))?
180            .try_into()?;
181        let code_commitment = code_commitment
182            .ok_or(proto::account::AccountHeader::missing_field(stringify!(code_commitment)))?
183            .try_into()?;
184
185        Ok(AccountHeader::new(
186            account_id,
187            Felt::new(nonce),
188            vault_root,
189            storage_commitment,
190            code_commitment,
191        ))
192    }
193}
194
195// ACCOUNT STORAGE HEADER
196// ================================================================================================
197
198impl TryInto<AccountStorageHeader> for proto::account::AccountStorageHeader {
199    type Error = crate::rpc::RpcError;
200
201    fn try_into(self) -> Result<AccountStorageHeader, Self::Error> {
202        use crate::rpc::domain::MissingFieldHelper;
203
204        let mut header_slots: Vec<(StorageSlotType, Word)> = Vec::with_capacity(self.slots.len());
205
206        for slot in self.slots {
207            let commitment: Word = slot
208                .commitment
209                .ok_or(proto::account::account_storage_header::StorageSlot::missing_field(
210                    stringify!(commitment),
211                ))?
212                .try_into()?;
213
214            let slot_type: StorageSlotType = SlotTypeProto(slot.slot_type).try_into()?;
215
216            header_slots.push((slot_type, commitment));
217        }
218
219        Ok(AccountStorageHeader::new(header_slots))
220    }
221}
222
223// FROM PROTO ACCOUNT HEADERS
224// ================================================================================================
225
226#[cfg(feature = "tonic")]
227impl proto::rpc_store::account_proof_response::AccountDetails {
228    /// Converts the RPC response into `StateHeaders`.
229    ///
230    /// The RPC response may omit unchanged account codes. If so, this function uses
231    /// `known_account_codes` to fill in the missing code. If a required code cannot be found in
232    /// the response or `known_account_codes`, an error is returned.
233    ///
234    /// # Errors
235    /// - If account code is missing both on `self` and `known_account_codes`
236    /// - If data cannot be correctly deserialized
237    pub fn into_domain(
238        self,
239        known_account_codes: &BTreeMap<Word, AccountCode>,
240    ) -> Result<AccountDetails, crate::rpc::RpcError> {
241        use crate::rpc::RpcError;
242        use crate::rpc::domain::MissingFieldHelper;
243
244        let proto::rpc_store::account_proof_response::AccountDetails {
245            header,
246            storage_details,
247            code,
248            vault_details,
249        } = self;
250        let header: AccountHeader = header
251            .ok_or(proto::rpc_store::account_proof_response::AccountDetails::missing_field(
252                stringify!(header),
253            ))?
254            .try_into()?;
255
256        let storage_details = storage_details
257            .ok_or(proto::rpc_store::account_proof_response::AccountDetails::missing_field(
258                stringify!(storage_details),
259            ))?
260            .try_into()?;
261
262        // If an account code was received, it means the previously known account code is no longer
263        // valid. If it was not, it means we sent a code commitment that matched and so our code
264        // is still valid
265        let code = {
266            let received_code = code.map(|c| AccountCode::read_from_bytes(&c)).transpose()?;
267            match received_code {
268                Some(code) => code,
269                None => known_account_codes
270                    .get(&header.code_commitment())
271                    .ok_or(RpcError::InvalidResponse(
272                        "Account code was not provided, but the response did not contain it either"
273                            .into(),
274                    ))?
275                    .clone(),
276            }
277        };
278
279        let vault_details = vault_details
280            .ok_or(proto::rpc_store::AccountVaultDetails::missing_field(stringify!(vault_details)))?
281            .try_into()?;
282
283        Ok(AccountDetails {
284            header,
285            storage_details,
286            code,
287            vault_details,
288        })
289    }
290}
291
292// ACCOUNT PROOF
293// ================================================================================================
294
295/// Contains a block number, and a list of account proofs at that block.
296pub type AccountProofs = (BlockNumber, Vec<AccountProof>);
297
298// ACCOUNT DETAILS
299// ================================================================================================
300
301/// An account details.
302#[derive(Clone, Debug)]
303pub struct AccountDetails {
304    pub header: AccountHeader,
305    pub storage_details: AccountStorageDetails,
306    pub code: AccountCode,
307    pub vault_details: AccountVaultDetails,
308}
309
310// ACCOUNT STORAGE DETAILS
311// ================================================================================================
312
313/// Account storage details for `AccountProofResponse`
314#[derive(Clone, Debug)]
315pub struct AccountStorageDetails {
316    /// Account storage header (storage slot info for up to 256 slots)
317    pub header: AccountStorageHeader,
318    /// Additional data for the requested storage maps
319    pub map_details: Vec<AccountStorageMapDetails>,
320}
321
322impl TryFrom<proto::rpc_store::AccountStorageDetails> for AccountStorageDetails {
323    type Error = RpcError;
324
325    fn try_from(value: proto::rpc_store::AccountStorageDetails) -> Result<Self, Self::Error> {
326        let header = value
327            .header
328            .ok_or(proto::account::AccountStorageHeader::missing_field(stringify!(header)))?
329            .try_into()?;
330        let map_details = value
331            .map_details
332            .iter()
333            .map(|entry| entry.to_owned().try_into())
334            .collect::<Result<Vec<AccountStorageMapDetails>, RpcError>>()?;
335
336        Ok(Self { header, map_details })
337    }
338}
339
340// ACCOUNT MAP DETAILS
341// ================================================================================================
342
343#[derive(Clone, Debug)]
344pub struct AccountStorageMapDetails {
345    /// slot index of the storage map
346    pub slot_index: u32,
347    /// A flag that is set to `true` if the number of to-be-returned entries in the
348    /// storage map would exceed a threshold. This indicates to the user that `SyncStorageMaps`
349    /// endpoint should be used to get all storage map data.
350    pub too_many_entries: bool,
351    /// By default we provide all storage entries.
352    pub entries: Vec<StorageMapEntry>,
353}
354
355impl TryFrom<proto::rpc_store::account_storage_details::AccountStorageMapDetails>
356    for AccountStorageMapDetails
357{
358    type Error = RpcError;
359
360    fn try_from(
361        value: proto::rpc_store::account_storage_details::AccountStorageMapDetails,
362    ) -> Result<Self, Self::Error> {
363        let slot_index = value.slot_index;
364        let too_many_entries = value.too_many_entries;
365
366        let entries = value
367            .entries
368            .ok_or(
369                proto::rpc_store::account_storage_details::AccountStorageMapDetails::missing_field(
370                    stringify!(entries),
371                ),
372            )?
373            .entries
374            .iter_mut()
375            .map(|entry| entry.to_owned().try_into())
376            .collect::<Result<Vec<StorageMapEntry>, RpcError>>()?;
377
378        Ok(Self { slot_index, too_many_entries, entries })
379    }
380}
381
382// STORAGE MAP ENTRY
383// ================================================================================================
384
385#[derive(Clone, Debug)]
386pub struct StorageMapEntry {
387    pub key: Word,
388    pub value: Word,
389}
390
391impl TryFrom<proto::rpc_store::account_storage_details::account_storage_map_details::map_entries::StorageMapEntry>
392    for StorageMapEntry
393{
394    type Error = RpcError;
395
396    fn try_from(value: proto::rpc_store::account_storage_details::account_storage_map_details::map_entries::StorageMapEntry) -> Result<Self, Self::Error> {
397        let key = value.key.ok_or(
398            proto::rpc_store::account_storage_details::account_storage_map_details::map_entries::StorageMapEntry::missing_field(
399                stringify!(key),
400            ))?.try_into()?;
401
402        let value = value.value.ok_or(
403            proto::rpc_store::account_storage_details::account_storage_map_details::map_entries::StorageMapEntry::missing_field(
404                stringify!(value),
405            ))?.try_into()?;
406
407        Ok(Self {
408            key,
409            value
410        })
411    }
412}
413
414// ACCOUNT VAULT DETAILS
415// ================================================================================================
416
417#[derive(Clone, Debug)]
418pub struct AccountVaultDetails {
419    /// A flag that is set to true if the account contains too many assets. This indicates
420    /// to the user that `SyncAccountVault` endpoint should be used to retrieve the
421    /// account's assets
422    pub too_many_assets: bool,
423    /// When `too_many_assets` == false, this will contain the list of assets in the
424    /// account's vault
425    pub assets: Vec<Asset>,
426}
427
428impl TryFrom<proto::rpc_store::AccountVaultDetails> for AccountVaultDetails {
429    type Error = RpcError;
430
431    fn try_from(value: proto::rpc_store::AccountVaultDetails) -> Result<Self, Self::Error> {
432        let too_many_assets = value.too_many_assets;
433        let assets = value
434            .assets
435            .into_iter()
436            .map(|asset| {
437                let native_digest: Word = asset
438                    .asset
439                    .ok_or(proto::rpc_store::AccountVaultDetails::missing_field(stringify!(
440                        assets
441                    )))?
442                    .try_into()?;
443                native_digest
444                    .try_into()
445                    .map_err(|_| RpcError::DeserializationError("asset".to_string()))
446            })
447            .collect::<Result<Vec<Asset>, RpcError>>()?;
448
449        Ok(Self { too_many_assets, assets })
450    }
451}
452
453// ACCOUNT PROOF
454// ================================================================================================
455
456/// Represents a proof of existence of an account's state at a specific block number.
457#[derive(Clone, Debug)]
458pub struct AccountProof {
459    /// Account witness.
460    account_witness: AccountWitness,
461    /// State headers of public accounts.
462    state_headers: Option<AccountDetails>,
463}
464
465impl AccountProof {
466    /// Creates a new [`AccountProof`].
467    pub fn new(
468        account_witness: AccountWitness,
469        account_details: Option<AccountDetails>,
470    ) -> Result<Self, AccountProofError> {
471        if let Some(AccountDetails {
472            header: account_header,
473            storage_details: _,
474            code,
475            ..
476        }) = &account_details
477        {
478            if account_header.commitment() != account_witness.state_commitment() {
479                return Err(AccountProofError::InconsistentAccountCommitment);
480            }
481            if account_header.id() != account_witness.id() {
482                return Err(AccountProofError::InconsistentAccountId);
483            }
484            if code.commitment() != account_header.code_commitment() {
485                return Err(AccountProofError::InconsistentCodeCommitment);
486            }
487        }
488
489        Ok(Self {
490            account_witness,
491            state_headers: account_details,
492        })
493    }
494
495    /// Returns the account ID related to the account proof.
496    pub fn account_id(&self) -> AccountId {
497        self.account_witness.id()
498    }
499
500    /// Returns the account header, if present.
501    pub fn account_header(&self) -> Option<&AccountHeader> {
502        self.state_headers.as_ref().map(|account_details| &account_details.header)
503    }
504
505    /// Returns the storage header, if present.
506    pub fn storage_header(&self) -> Option<&AccountStorageHeader> {
507        self.state_headers
508            .as_ref()
509            .map(|account_details| &account_details.storage_details.header)
510    }
511
512    /// Returns the account code, if present.
513    pub fn account_code(&self) -> Option<&AccountCode> {
514        self.state_headers.as_ref().map(|headers| &headers.code)
515    }
516
517    /// Returns the code commitment, if account code is present in the state headers.
518    pub fn code_commitment(&self) -> Option<Word> {
519        self.account_code().map(AccountCode::commitment)
520    }
521
522    /// Returns the current state commitment of the account.
523    pub fn account_commitment(&self) -> Word {
524        self.account_witness.state_commitment()
525    }
526
527    pub fn account_witness(&self) -> &AccountWitness {
528        &self.account_witness
529    }
530
531    /// Returns the proof of the account's inclusion.
532    pub fn merkle_proof(&self) -> &SparseMerklePath {
533        self.account_witness.path()
534    }
535
536    /// Deconstructs `AccountProof` into its individual parts.
537    pub fn into_parts(self) -> (AccountWitness, Option<AccountDetails>) {
538        (self.account_witness, self.state_headers)
539    }
540}
541
542// ACCOUNT WITNESS
543// ================================================================================================
544
545impl TryFrom<proto::account::AccountWitness> for AccountWitness {
546    type Error = RpcError;
547
548    fn try_from(account_witness: proto::account::AccountWitness) -> Result<Self, Self::Error> {
549        let state_commitment = account_witness
550            .commitment
551            .ok_or(proto::account::AccountWitness::missing_field(stringify!(state_commitment)))?
552            .try_into()?;
553        let merkle_path = account_witness
554            .path
555            .ok_or(proto::account::AccountWitness::missing_field(stringify!(merkle_path)))?
556            .try_into()?;
557        let account_id = account_witness
558            .witness_id
559            .ok_or(proto::account::AccountWitness::missing_field(stringify!(witness_id)))?
560            .try_into()?;
561
562        let witness = AccountWitness::new(account_id, state_commitment, merkle_path).unwrap();
563        Ok(witness)
564    }
565}
566
567// ACCOUNT STORAGE REQUEST
568// ================================================================================================
569
570pub type StorageSlotIndex = u8;
571pub type StorageMapKey = Word;
572
573/// Describes storage slots indices to be requested, as well as a list of keys for each of those
574/// slots.
575#[derive(Clone, Debug, Default, Eq, PartialEq)]
576pub struct AccountStorageRequirements(BTreeMap<StorageSlotIndex, Vec<StorageMapKey>>);
577
578impl AccountStorageRequirements {
579    pub fn new<'a>(
580        slots_and_keys: impl IntoIterator<
581            Item = (StorageSlotIndex, impl IntoIterator<Item = &'a StorageMapKey>),
582        >,
583    ) -> Self {
584        let map = slots_and_keys
585            .into_iter()
586            .map(|(slot_index, keys_iter)| {
587                let keys_vec: Vec<StorageMapKey> = keys_iter.into_iter().copied().collect();
588                (slot_index, keys_vec)
589            })
590            .collect();
591
592        AccountStorageRequirements(map)
593    }
594
595    pub fn inner(&self) -> &BTreeMap<StorageSlotIndex, Vec<StorageMapKey>> {
596        &self.0
597    }
598}
599
600impl From<AccountStorageRequirements>
601    for Vec<
602        proto::rpc_store::account_proof_request::account_detail_request::StorageMapDetailRequest,
603    >
604{
605    fn from(
606        value: AccountStorageRequirements,
607    ) -> Vec<proto::rpc_store::account_proof_request::account_detail_request::StorageMapDetailRequest>
608    {
609        use proto::rpc_store::account_proof_request::account_detail_request;
610        use proto::rpc_store::account_proof_request::account_detail_request::storage_map_detail_request;
611        let request_map = value.0;
612        let mut requests = Vec::with_capacity(request_map.len());
613        for (slot_index, map_keys) in request_map {
614            let map_keys = storage_map_detail_request::MapKeys {
615                map_keys: map_keys
616                    .into_iter()
617                    .map(crate::rpc::generated::primitives::Digest::from)
618                    .collect(),
619            };
620            let slot_data = Some(storage_map_detail_request::SlotData::MapKeys(map_keys));
621            requests.push(account_detail_request::StorageMapDetailRequest {
622                slot_index: u32::from(slot_index),
623                slot_data,
624            });
625        }
626        requests
627    }
628}
629
630impl Serializable for AccountStorageRequirements {
631    fn write_into<W: miden_tx::utils::ByteWriter>(&self, target: &mut W) {
632        target.write(&self.0);
633    }
634}
635
636impl Deserializable for AccountStorageRequirements {
637    fn read_from<R: miden_tx::utils::ByteReader>(
638        source: &mut R,
639    ) -> Result<Self, miden_tx::utils::DeserializationError> {
640        Ok(AccountStorageRequirements(source.read()?))
641    }
642}
643
644// ERRORS
645// ================================================================================================
646
647#[derive(Debug, Error)]
648pub enum AccountProofError {
649    #[error(
650        "the received account commitment doesn't match the received account header's commitment"
651    )]
652    InconsistentAccountCommitment,
653    #[error("the received account id doesn't match the received account header's id")]
654    InconsistentAccountId,
655    #[error(
656        "the received code commitment doesn't match the received account header's code commitment"
657    )]
658    InconsistentCodeCommitment,
659}