1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3use miden_protocol::account::{
4 Account, AccountCode, AccountHeader, AccountId, AccountStorage, AccountStorageHeader,
5 StorageMap, StorageMapKey, StorageSlot, StorageSlotName, StorageSlotType,
6};
7use miden_protocol::asset::{Asset, AssetVault};
8use miden_protocol::block::BlockNumber;
9use miden_protocol::block::account_tree::AccountWitness;
10use miden_protocol::crypto::merkle::SparseMerklePath;
11use miden_protocol::crypto::merkle::smt::PartialSmt;
12use miden_objects::DecodeMessageExt;
13use miden_protocol::{EMPTY_WORD, Word};
14use miden_tx::utils::serde::{Deserializable, Serializable};
15use thiserror::Error;
16
17use crate::alloc::string::ToString;
18use crate::rpc::{AccountStateAt, RpcError};
19use crate::rpc::domain::MissingFieldHelper;
20use crate::rpc::generated::rpc::account_request::account_detail_request::storage_map_detail_request::{MapKeys, SlotData};
21use crate::rpc::generated::rpc::account_request::account_detail_request::{
22 StorageMapDetailRequest, StorageMapDetailRequests, StorageRequest,
23};
24use crate::rpc::generated::{self as proto};
25
26#[cfg(feature = "tonic")]
30impl proto::rpc::account_response::AccountDetails {
31 pub fn into_domain(
45 self,
46 known_account_codes: &BTreeMap<Word, AccountCode>,
47 storage_requirements: &AccountStorageRequirements,
48 ) -> Result<AccountDetails, crate::rpc::RpcError> {
49 use crate::rpc::RpcError;
50 use crate::rpc::domain::MissingFieldHelper;
51
52 let proto::rpc::account_response::AccountDetails {
53 header,
54 storage_details,
55 code,
56 vault_details,
57 } = self;
58 let header: AccountHeader = header
59 .ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!(header)))?
60 .decode_and_verify()?;
61
62 let storage_details: AccountStorageDetails = storage_details
63 .ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!(
64 storage_details
65 )))?
66 .try_into()?;
67
68 storage_details.validate_against_request(storage_requirements)?;
69
70 let code = {
74 let received_code: Option<AccountCode> =
75 code.map(DecodeMessageExt::decode_and_verify).transpose()?;
76 match received_code {
77 Some(code) => code,
78 None => known_account_codes
79 .get(&header.code_commitment())
80 .ok_or(RpcError::InvalidResponse(
81 "Account code was not provided, but the response did not contain it either"
82 .into(),
83 ))?
84 .clone(),
85 }
86 };
87
88 let vault_details = vault_details
89 .ok_or(proto::rpc::AccountVaultDetails::missing_field(stringify!(vault_details)))?
90 .try_into()?;
91
92 Ok(AccountDetails {
93 header,
94 storage_details,
95 code,
96 vault_details,
97 })
98 }
99}
100
101pub type AccountProofs = (BlockNumber, Vec<AccountProof>);
106
107#[derive(Clone, Debug)]
112pub struct AccountDetails {
113 pub header: AccountHeader,
114 pub storage_details: AccountStorageDetails,
115 pub code: AccountCode,
116 pub vault_details: AccountVaultDetails,
117}
118
119impl TryFrom<&AccountDetails> for Account {
120 type Error = RpcError;
121
122 fn try_from(details: &AccountDetails) -> Result<Self, Self::Error> {
128 if details.vault_details.too_many_assets {
129 return Err(RpcError::ExpectedDataMissing(
130 "cannot build account: vault has too many assets".into(),
131 ));
132 }
133
134 if let Some(slot_name) = details
135 .storage_details
136 .map_details
137 .iter()
138 .find(|m| m.is_limit_exceeded())
139 .map(|m| &m.slot_name)
140 {
141 return Err(RpcError::ExpectedDataMissing(format!(
142 "cannot build account: storage map slot '{slot_name}' has too many entries",
143 )));
144 }
145
146 let mut slots: Vec<StorageSlot> = Vec::new();
147
148 for slot_header in details.storage_details.header.slots() {
149 match slot_header.slot_type() {
150 StorageSlotType::Value => {
151 slots.push(StorageSlot::with_value(
152 slot_header.name().clone(),
153 slot_header.value(),
154 ));
155 },
156 StorageSlotType::Map => {
157 let map_details = details
158 .storage_details
159 .find_map_details(slot_header.name())
160 .ok_or_else(|| {
161 RpcError::ExpectedDataMissing(format!(
162 "slot '{}' is a map but has no map_details in response",
163 slot_header.name()
164 ))
165 })?;
166
167 let storage_map = map_details
168 .entries
169 .clone()
170 .into_storage_map()
171 .ok_or_else(|| {
172 RpcError::ExpectedDataMissing(format!(
173 "slot '{}' did not come back with all its entries, so the full \
174 account cannot be built",
175 slot_header.name(),
176 ))
177 })?
178 .map_err(|err| {
179 RpcError::InvalidResponse(format!(
180 "the rpc api returned a non-valid map entry: {err}"
181 ))
182 })?;
183
184 slots.push(StorageSlot::with_map(slot_header.name().clone(), storage_map));
185 },
186 }
187 }
188
189 let asset_vault = AssetVault::new(&details.vault_details.assets).map_err(|err| {
190 RpcError::InvalidResponse(format!("rpc api returned non-valid assets: {err}"))
191 })?;
192
193 let account_storage = AccountStorage::new(slots).map_err(|err| {
194 RpcError::InvalidResponse(format!("rpc api returned non-valid storage slots: {err}"))
195 })?;
196
197 Account::new(
198 details.header.id(),
199 asset_vault,
200 account_storage,
201 details.code.clone(),
202 details.header.nonce(),
203 None,
204 )
205 .map_err(|err| {
206 RpcError::InvalidResponse(format!(
207 "failed to construct account from rpc api response: {err}"
208 ))
209 })
210 }
211}
212
213#[derive(Clone, Debug)]
218pub struct AccountStorageDetails {
219 pub header: AccountStorageHeader,
221 pub map_details: Vec<AccountStorageMapDetails>,
223}
224
225impl AccountStorageDetails {
226 pub fn find_map_details(&self, target: &StorageSlotName) -> Option<&AccountStorageMapDetails> {
230 self.map_details.iter().find(|map_detail| map_detail.slot_name == *target)
231 }
232
233 pub fn validate_against_request(
239 &self,
240 storage_requirements: &AccountStorageRequirements,
241 ) -> Result<(), RpcError> {
242 for map_detail in &self.map_details {
243 let StorageMapEntries::PartialMap { map_keys, .. } = &map_detail.entries else {
244 continue;
245 };
246
247 let requested_keys = storage_requirements.keys_for_slot(&map_detail.slot_name);
248 if let Some(key) = requested_keys.iter().find(|key| !map_keys.contains(key)) {
249 return Err(RpcError::InvalidResponse(format!(
250 "partial storage map for slot '{}' does not cover requested key {}",
251 map_detail.slot_name,
252 key.to_hex(),
253 )));
254 }
255 if let Some(key) = map_keys.iter().find(|key| !requested_keys.contains(key)) {
256 return Err(RpcError::InvalidResponse(format!(
257 "partial storage map for slot '{}' covers key {}, which was not requested",
258 map_detail.slot_name,
259 key.to_hex(),
260 )));
261 }
262 }
263
264 Ok(())
265 }
266}
267
268impl TryFrom<proto::rpc::AccountStorageDetails> for AccountStorageDetails {
269 type Error = RpcError;
270
271 fn try_from(value: proto::rpc::AccountStorageDetails) -> Result<Self, Self::Error> {
272 let header: AccountStorageHeader = value
273 .header
274 .ok_or(proto::account::AccountStorageHeader::missing_field(stringify!(header)))?
275 .decode_and_verify()?;
276 let map_details = value
277 .map_details
278 .into_iter()
279 .map(core::convert::TryInto::try_into)
280 .collect::<Result<Vec<AccountStorageMapDetails>, RpcError>>()?;
281
282 for map_detail in &map_details {
286 let StorageMapEntries::PartialMap { partial_smt, .. } = &map_detail.entries else {
287 continue;
288 };
289
290 let slot = header
291 .slots()
292 .find(|slot| *slot.name() == map_detail.slot_name)
293 .ok_or_else(|| {
294 RpcError::InvalidResponse(format!(
295 "partial storage map references slot '{}', which is absent from the \
296 storage header",
297 map_detail.slot_name,
298 ))
299 })?;
300 if slot.slot_type() != StorageSlotType::Map {
301 return Err(RpcError::InvalidResponse(format!(
302 "partial storage map references slot '{}', which is not a map",
303 map_detail.slot_name,
304 )));
305 }
306 if partial_smt.root() != slot.value() {
307 return Err(RpcError::InvalidResponse(format!(
308 "partial storage map for slot '{}' has root {} but the storage header reports \
309 {}",
310 map_detail.slot_name,
311 partial_smt.root(),
312 slot.value(),
313 )));
314 }
315 }
316
317 Ok(Self { header, map_details })
318 }
319}
320
321#[derive(Clone, Debug)]
325pub struct AccountStorageMapDetails {
326 pub slot_name: StorageSlotName,
328 pub entries: StorageMapEntries,
330}
331
332impl AccountStorageMapDetails {
333 pub const MAX_PARTIAL_MAP_KEYS: usize = 64;
336
337 pub fn is_limit_exceeded(&self) -> bool {
341 matches!(self.entries, StorageMapEntries::LimitExceeded)
342 }
343}
344
345impl TryFrom<proto::rpc::account_storage_details::AccountStorageMapDetails>
346 for AccountStorageMapDetails
347{
348 type Error = RpcError;
349
350 fn try_from(
351 value: proto::rpc::account_storage_details::AccountStorageMapDetails,
352 ) -> Result<Self, Self::Error> {
353 use proto::rpc::account_storage_details::account_storage_map_details::Result as ProtoResult;
354
355 let slot_name = StorageSlotName::new(value.slot_name)
356 .map_err(|err| RpcError::ExpectedDataMissing(err.to_string()))?;
357
358 let entries = match value.result {
359 Some(ProtoResult::TooManyEntries(true)) => StorageMapEntries::LimitExceeded,
360 Some(ProtoResult::TooManyEntries(false)) => {
361 return Err(RpcError::InvalidResponse(
362 "too_many_entries must be true when set".into(),
363 ));
364 },
365 Some(ProtoResult::AllEntries(all_entries)) => {
366 let entries = all_entries
367 .entries
368 .into_iter()
369 .map(core::convert::TryInto::try_into)
370 .collect::<Result<Vec<StorageMapEntry>, RpcError>>()?;
371 StorageMapEntries::AllEntries(entries)
372 },
373 Some(ProtoResult::PartialMap(partial_map)) => {
374 if partial_map.map_keys.len() > Self::MAX_PARTIAL_MAP_KEYS {
375 return Err(RpcError::InvalidResponse(format!(
376 "partial storage map for slot '{slot_name}' contains {} keys, exceeding \
377 the limit of {}",
378 partial_map.map_keys.len(),
379 Self::MAX_PARTIAL_MAP_KEYS,
380 )));
381 }
382
383 let map_keys = partial_map
384 .map_keys
385 .into_iter()
386 .map(|key| Word::try_from(key).map(StorageMapKey::new))
387 .collect::<Result<Vec<_>, _>>()?;
388 if let Some(key) = first_duplicate_key(&map_keys) {
389 return Err(RpcError::InvalidResponse(format!(
390 "partial storage map for slot '{slot_name}' repeats key {}",
391 key.to_hex(),
392 )));
393 }
394
395 let partial_smt: PartialSmt = partial_map
396 .partial_smt
397 .ok_or(proto::rpc::account_storage_details::account_storage_map_details::PartialStorageMap::missing_field(
398 stringify!(partial_smt),
399 ))?
400 .decode_and_verify()?;
401
402 for key in &map_keys {
405 partial_smt.get_value(&key.hash().as_word()).map_err(|_| {
406 RpcError::InvalidResponse(format!(
407 "partial storage map for slot '{slot_name}' does not track key {}",
408 key.to_hex(),
409 ))
410 })?;
411 }
412
413 StorageMapEntries::PartialMap { map_keys, partial_smt }
414 },
415 None => {
416 return Err(RpcError::InvalidResponse(format!(
417 "storage map details for slot '{slot_name}' carry no result",
418 )));
419 },
420 };
421
422 Ok(Self { slot_name, entries })
423 }
424}
425
426fn first_duplicate_key(keys: &[StorageMapKey]) -> Option<&StorageMapKey> {
431 keys.iter()
432 .enumerate()
433 .find_map(|(index, key)| keys[..index].contains(key).then_some(key))
434}
435
436#[derive(Clone, Debug)]
441pub struct StorageMapEntry {
442 pub key: StorageMapKey,
443 pub value: Word,
444}
445
446impl TryFrom<proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry>
447 for StorageMapEntry
448{
449 type Error = RpcError;
450
451 fn try_from(value: proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry) -> Result<Self, Self::Error> {
452 let key = Word::try_from(value.key.ok_or(RpcError::ExpectedDataMissing("key".into()))?)
453 .map(StorageMapKey::new)?;
454 let value = value.value.ok_or(RpcError::ExpectedDataMissing("value".into()))?.try_into()?;
455 Ok(Self { key, value })
456 }
457}
458
459#[derive(Clone, Debug)]
465pub enum StorageMapEntries {
466 LimitExceeded,
469 AllEntries(Vec<StorageMapEntry>),
471 PartialMap {
477 map_keys: Vec<StorageMapKey>,
479 partial_smt: PartialSmt,
481 },
482}
483
484impl StorageMapEntries {
485 pub fn into_storage_map(
490 self,
491 ) -> Option<Result<StorageMap, miden_protocol::errors::StorageMapError>> {
492 match self {
493 StorageMapEntries::AllEntries(entries) => {
494 Some(StorageMap::with_entries(entries.into_iter().map(|e| (e.key, e.value))))
495 },
496 StorageMapEntries::LimitExceeded | StorageMapEntries::PartialMap { .. } => None,
497 }
498 }
499}
500
501#[derive(Clone, Debug)]
505pub struct AccountVaultDetails {
506 pub too_many_assets: bool,
509 pub assets: Vec<Asset>,
511}
512
513impl TryFrom<proto::rpc::AccountVaultDetails> for AccountVaultDetails {
514 type Error = RpcError;
515
516 fn try_from(value: proto::rpc::AccountVaultDetails) -> Result<Self, Self::Error> {
517 let too_many_assets = value.too_many_assets;
518 let assets = value
519 .assets
520 .into_iter()
521 .map(DecodeMessageExt::decode_and_verify)
522 .collect::<Result<Vec<Asset>, _>>()?;
523
524 Ok(Self { too_many_assets, assets })
525 }
526}
527
528#[derive(Clone, Debug)]
533pub struct AccountProof {
534 account_witness: AccountWitness,
536 state_headers: Option<AccountDetails>,
538}
539
540impl AccountProof {
541 pub fn new(
543 account_witness: AccountWitness,
544 account_details: Option<AccountDetails>,
545 ) -> Result<Self, AccountProofError> {
546 if let Some(AccountDetails {
547 header: account_header,
548 storage_details: _,
549 code,
550 ..
551 }) = &account_details
552 {
553 if account_header.to_commitment() != account_witness.state_commitment() {
554 return Err(AccountProofError::InconsistentAccountCommitment);
555 }
556 if account_header.id() != account_witness.id() {
557 return Err(AccountProofError::InconsistentAccountId);
558 }
559 if code.commitment() != account_header.code_commitment() {
560 return Err(AccountProofError::InconsistentCodeCommitment);
561 }
562 }
563
564 Ok(Self {
565 account_witness,
566 state_headers: account_details,
567 })
568 }
569
570 pub fn account_id(&self) -> AccountId {
572 self.account_witness.id()
573 }
574
575 pub fn account_header(&self) -> Option<&AccountHeader> {
577 self.state_headers.as_ref().map(|account_details| &account_details.header)
578 }
579
580 pub fn storage_header(&self) -> Option<&AccountStorageHeader> {
582 self.state_headers
583 .as_ref()
584 .map(|account_details| &account_details.storage_details.header)
585 }
586
587 pub fn storage_details(&self) -> Option<&AccountStorageDetails> {
589 self.state_headers.as_ref().map(|d| &d.storage_details)
590 }
591
592 pub fn vault_details(&self) -> Option<&AccountVaultDetails> {
594 self.state_headers.as_ref().map(|d| &d.vault_details)
595 }
596
597 pub fn find_map_details(
599 &self,
600 slot_name: &StorageSlotName,
601 ) -> Option<&AccountStorageMapDetails> {
602 self.state_headers
603 .as_ref()
604 .and_then(|details| details.storage_details.find_map_details(slot_name))
605 }
606
607 pub fn account_code(&self) -> Option<&AccountCode> {
609 self.state_headers.as_ref().map(|headers| &headers.code)
610 }
611
612 pub fn code_commitment(&self) -> Option<Word> {
614 self.account_code().map(AccountCode::commitment)
615 }
616
617 pub fn account_commitment(&self) -> Word {
619 self.account_witness.state_commitment()
620 }
621
622 pub fn account_witness(&self) -> &AccountWitness {
623 &self.account_witness
624 }
625
626 pub fn merkle_proof(&self) -> &SparseMerklePath {
628 self.account_witness.path()
629 }
630
631 pub fn into_parts(self) -> (AccountWitness, Option<AccountDetails>) {
633 (self.account_witness, self.state_headers)
634 }
635
636 pub fn into_details(self) -> Option<AccountDetails> {
638 self.state_headers
639 }
640
641 pub fn details_mut(&mut self) -> Option<&mut AccountDetails> {
647 self.state_headers.as_mut()
648 }
649}
650
651#[cfg(feature = "tonic")]
652impl TryFrom<proto::rpc::AccountResponse> for AccountProof {
653 type Error = RpcError;
654 fn try_from(account_proof: proto::rpc::AccountResponse) -> Result<Self, Self::Error> {
655 let Some(witness) = account_proof.witness else {
656 return Err(RpcError::ExpectedDataMissing(
657 "GetAccount returned an account without witness".to_string(),
658 ));
659 };
660
661 let details: Option<AccountDetails> = {
662 match account_proof.details {
663 None => None,
664 Some(details) => Some(
665 details
666 .into_domain(&BTreeMap::new(), &AccountStorageRequirements::default())?,
667 ),
668 }
669 };
670 AccountProof::new(witness.decode_and_verify()?, details)
671 .map_err(|err| RpcError::InvalidResponse(format!("{err}")))
672 }
673}
674
675#[derive(Clone, Debug, Default, Eq, PartialEq)]
684pub struct AccountStorageRequirements(BTreeMap<StorageSlotName, Vec<StorageMapKey>>);
685
686impl AccountStorageRequirements {
687 pub fn new<'a>(
693 slots_and_keys: impl IntoIterator<
694 Item = (StorageSlotName, impl IntoIterator<Item = &'a StorageMapKey>),
695 >,
696 ) -> Self {
697 let map = slots_and_keys
698 .into_iter()
699 .map(|(slot_name, keys_iter)| {
700 let mut keys_vec: Vec<StorageMapKey> = Vec::new();
701 for key in keys_iter {
702 if !keys_vec.contains(key) {
703 keys_vec.push(*key);
704 }
705 }
706 (slot_name, keys_vec)
707 })
708 .collect();
709
710 AccountStorageRequirements(map)
711 }
712
713 pub fn all_entries(slot_names: &[StorageSlotName]) -> Self {
716 AccountStorageRequirements(
717 slot_names.iter().map(|name| (name.clone(), Vec::new())).collect(),
718 )
719 }
720
721 pub fn inner(&self) -> &BTreeMap<StorageSlotName, Vec<StorageMapKey>> {
722 &self.0
723 }
724
725 pub fn keys_for_slot(&self, slot_name: &StorageSlotName) -> &[StorageMapKey] {
727 self.0.get(slot_name).map_or(&[], Vec::as_slice)
728 }
729}
730
731impl From<AccountStorageRequirements> for Vec<StorageMapDetailRequest> {
732 fn from(value: AccountStorageRequirements) -> Vec<StorageMapDetailRequest> {
733 let request_map = value.0;
734 let mut requests = Vec::with_capacity(request_map.len());
735 for (slot_name, map_keys) in request_map {
736 let slot_data = if map_keys.is_empty() {
737 Some(SlotData::AllEntries(true))
738 } else {
739 let keys = map_keys.into_iter().map(|key| Word::from(key).into()).collect();
740 Some(SlotData::MapKeys(MapKeys { map_keys: keys }))
741 };
742 requests.push(StorageMapDetailRequest {
743 slot_name: slot_name.to_string(),
744 slot_data,
745 });
746 }
747 requests
748 }
749}
750
751impl Serializable for AccountStorageRequirements {
752 fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
753 target.write(&self.0);
754 }
755}
756
757impl Deserializable for AccountStorageRequirements {
758 fn read_from<R: miden_tx::utils::serde::ByteReader>(
759 source: &mut R,
760 ) -> Result<Self, miden_tx::utils::serde::DeserializationError> {
761 Ok(AccountStorageRequirements(source.read()?))
762 }
763}
764
765#[derive(Clone, Debug, Default)]
770pub enum VaultFetch {
771 #[default]
773 Skip,
774 Always,
776 IfChangedFrom(Word),
781}
782
783impl From<VaultFetch> for Option<proto::primitives::Word> {
784 fn from(vault: VaultFetch) -> Self {
788 match vault {
789 VaultFetch::Skip => None,
790 VaultFetch::Always => Some(EMPTY_WORD.into()),
791 VaultFetch::IfChangedFrom(commitment) => Some(commitment.into()),
792 }
793 }
794}
795
796#[derive(Clone, Debug, Default)]
802pub enum StorageMapFetch {
803 #[default]
805 Skip,
806 All,
810 Slots(AccountStorageRequirements),
813}
814
815impl From<StorageMapFetch> for Option<StorageRequest> {
816 fn from(storage: StorageMapFetch) -> Self {
817 match storage {
818 StorageMapFetch::Skip => None,
819 StorageMapFetch::All => Some(StorageRequest::AllStorageMaps(true)),
820 StorageMapFetch::Slots(reqs) => {
821 Some(StorageRequest::StorageMaps(StorageMapDetailRequests {
822 storage_maps: reqs.into(),
823 }))
824 },
825 }
826 }
827}
828
829#[derive(Clone, Debug, Default)]
831pub struct GetAccountRequest {
832 pub storage: StorageMapFetch,
834 pub at: AccountStateAt,
836 pub known_code: Option<AccountCode>,
839 pub vault: VaultFetch,
841}
842
843impl GetAccountRequest {
844 #[must_use]
848 pub fn new() -> Self {
849 Self {
850 storage: StorageMapFetch::Skip,
851 at: AccountStateAt::ChainTip,
852 known_code: None,
853 vault: VaultFetch::Skip,
854 }
855 }
856
857 #[must_use]
859 pub fn with_storage(mut self, storage: StorageMapFetch) -> Self {
860 self.storage = storage;
861 self
862 }
863
864 #[must_use]
866 pub fn at(mut self, at: AccountStateAt) -> Self {
867 self.at = at;
868 self
869 }
870
871 #[must_use]
874 pub fn with_known_code(mut self, known_code: Option<AccountCode>) -> Self {
875 self.known_code = known_code;
876 self
877 }
878
879 #[must_use]
881 pub fn with_vault(mut self, vault: VaultFetch) -> Self {
882 self.vault = vault;
883 self
884 }
885}
886
887#[derive(Debug, Error)]
891pub enum AccountProofError {
892 #[error(
893 "the received account commitment doesn't match the received account header's commitment"
894 )]
895 InconsistentAccountCommitment,
896 #[error("the received account id doesn't match the received account header's id")]
897 InconsistentAccountId,
898 #[error(
899 "the received code commitment doesn't match the received account header's code commitment"
900 )]
901 InconsistentCodeCommitment,
902}