1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3use core::fmt::{self, Debug, Formatter};
4
5use miden_protocol::account::{
6 Account, AccountCode, AccountHeader, AccountId, AccountStorage, AccountStorageHeader,
7 StorageMap, StorageMapKey, StorageSlot, StorageSlotName, StorageSlotType,
8};
9use miden_protocol::asset::{Asset, AssetVault};
10use miden_protocol::block::account_tree::AccountWitness;
11use miden_protocol::crypto::merkle::SparseMerklePath;
12use miden_protocol::crypto::merkle::smt::PartialSmt;
13use miden_objects::DecodeMessageExt;
14use miden_protocol::{EMPTY_WORD, Word};
15use miden_tx::utils::serde::{Deserializable, Serializable};
16use thiserror::Error;
17
18use crate::alloc::string::ToString;
19use crate::rpc::{AccountStateAt, RpcError};
20use crate::rpc::domain::MissingFieldHelper;
21use crate::rpc::generated::rpc::get_account_request::account_detail_request::storage_map_detail_request::{MapKeys, SlotData};
22use crate::rpc::generated::rpc::get_account_request::account_detail_request::{
23 StorageMapDetailRequest, StorageMapDetailRequests, StorageRequest,
24};
25use crate::rpc::generated::{self as proto};
26
27impl Debug for proto::rpc::RegisterAccountRequest {
32 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
33 f.debug_struct("RegisterAccountRequest")
34 .field("account_id", &self.account_id)
35 .finish_non_exhaustive()
36 }
37}
38
39impl Debug for proto::rpc::IsInvitationCodeValidRequest {
41 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
42 f.debug_struct("IsInvitationCodeValidRequest").finish_non_exhaustive()
43 }
44}
45
46#[cfg(feature = "tonic")]
50impl proto::rpc::get_account_response::AccountDetails {
51 pub fn into_domain(
65 self,
66 known_account_codes: &BTreeMap<Word, AccountCode>,
67 storage_requirements: &AccountStorageRequirements,
68 ) -> Result<AccountDetails, crate::rpc::RpcError> {
69 use crate::rpc::RpcError;
70 use crate::rpc::domain::MissingFieldHelper;
71
72 let proto::rpc::get_account_response::AccountDetails {
73 header,
74 storage_details,
75 code,
76 vault_details,
77 } = self;
78 let header: AccountHeader = header
79 .ok_or(proto::rpc::get_account_response::AccountDetails::missing_field(stringify!(
80 header
81 )))?
82 .decode_and_verify()?;
83
84 let storage_details: AccountStorageDetails = storage_details
85 .ok_or(proto::rpc::get_account_response::AccountDetails::missing_field(stringify!(
86 storage_details
87 )))?
88 .try_into()?;
89
90 storage_details.validate_against_request(storage_requirements)?;
91
92 let code = {
96 let received_code: Option<AccountCode> =
97 code.map(DecodeMessageExt::decode_and_verify).transpose()?;
98 match received_code {
99 Some(code) => code,
100 None => known_account_codes
101 .get(&header.code_commitment())
102 .ok_or(RpcError::InvalidResponse(
103 "Account code was not provided, but the response did not contain it either"
104 .into(),
105 ))?
106 .clone(),
107 }
108 };
109
110 let vault_details = vault_details
111 .ok_or(proto::rpc::AccountVaultDetails::missing_field(stringify!(vault_details)))?
112 .try_into()?;
113
114 Ok(AccountDetails {
115 header,
116 storage_details,
117 code,
118 vault_details,
119 })
120 }
121}
122
123#[derive(Clone, Debug)]
128pub struct AccountDetails {
129 pub header: AccountHeader,
130 pub storage_details: AccountStorageDetails,
131 pub code: AccountCode,
132 pub vault_details: AccountVaultDetails,
133}
134
135impl TryFrom<&AccountDetails> for Account {
136 type Error = RpcError;
137
138 fn try_from(details: &AccountDetails) -> Result<Self, Self::Error> {
144 if details.vault_details.too_many_assets {
145 return Err(RpcError::ExpectedDataMissing(
146 "cannot build account: vault has too many assets".into(),
147 ));
148 }
149
150 if let Some(slot_name) = details
151 .storage_details
152 .map_details
153 .iter()
154 .find(|m| m.is_limit_exceeded())
155 .map(|m| &m.slot_name)
156 {
157 return Err(RpcError::ExpectedDataMissing(format!(
158 "cannot build account: storage map slot '{slot_name}' has too many entries",
159 )));
160 }
161
162 let mut slots: Vec<StorageSlot> = Vec::new();
163
164 for slot_header in details.storage_details.header.slots() {
165 match slot_header.slot_type() {
166 StorageSlotType::Value => {
167 slots.push(StorageSlot::with_value(
168 slot_header.name().clone(),
169 slot_header.value(),
170 ));
171 },
172 StorageSlotType::Map => {
173 let map_details = details
174 .storage_details
175 .find_map_details(slot_header.name())
176 .ok_or_else(|| {
177 RpcError::ExpectedDataMissing(format!(
178 "slot '{}' is a map but has no map_details in response",
179 slot_header.name()
180 ))
181 })?;
182
183 let storage_map = map_details
184 .entries
185 .clone()
186 .into_storage_map()
187 .ok_or_else(|| {
188 RpcError::ExpectedDataMissing(format!(
189 "slot '{}' did not come back with all its entries, so the full \
190 account cannot be built",
191 slot_header.name(),
192 ))
193 })?
194 .map_err(|err| {
195 RpcError::InvalidResponse(format!(
196 "the rpc api returned a non-valid map entry: {err}"
197 ))
198 })?;
199
200 slots.push(StorageSlot::with_map(slot_header.name().clone(), storage_map));
201 },
202 }
203 }
204
205 let asset_vault = AssetVault::new(&details.vault_details.assets).map_err(|err| {
206 RpcError::InvalidResponse(format!("rpc api returned non-valid assets: {err}"))
207 })?;
208
209 let account_storage = AccountStorage::new(slots).map_err(|err| {
210 RpcError::InvalidResponse(format!("rpc api returned non-valid storage slots: {err}"))
211 })?;
212
213 Account::new(
214 details.header.id(),
215 asset_vault,
216 account_storage,
217 details.code.clone(),
218 details.header.nonce(),
219 None,
220 )
221 .map_err(|err| {
222 RpcError::InvalidResponse(format!(
223 "failed to construct account from rpc api response: {err}"
224 ))
225 })
226 }
227}
228
229#[derive(Clone, Debug)]
234pub struct AccountStorageDetails {
235 pub header: AccountStorageHeader,
237 pub map_details: Vec<AccountStorageMapDetails>,
239}
240
241impl AccountStorageDetails {
242 pub fn find_map_details(&self, target: &StorageSlotName) -> Option<&AccountStorageMapDetails> {
246 self.map_details.iter().find(|map_detail| map_detail.slot_name == *target)
247 }
248
249 pub fn validate_against_request(
255 &self,
256 storage_requirements: &AccountStorageRequirements,
257 ) -> Result<(), RpcError> {
258 for map_detail in &self.map_details {
259 let StorageMapEntries::PartialMap { map_keys, .. } = &map_detail.entries else {
260 continue;
261 };
262
263 let requested_keys = storage_requirements.keys_for_slot(&map_detail.slot_name);
264 if let Some(key) = requested_keys.iter().find(|key| !map_keys.contains(key)) {
265 return Err(RpcError::InvalidResponse(format!(
266 "partial storage map for slot '{}' does not cover requested key {}",
267 map_detail.slot_name,
268 key.to_hex(),
269 )));
270 }
271 if let Some(key) = map_keys.iter().find(|key| !requested_keys.contains(key)) {
272 return Err(RpcError::InvalidResponse(format!(
273 "partial storage map for slot '{}' covers key {}, which was not requested",
274 map_detail.slot_name,
275 key.to_hex(),
276 )));
277 }
278 }
279
280 Ok(())
281 }
282}
283
284impl TryFrom<proto::rpc::AccountStorageDetails> for AccountStorageDetails {
285 type Error = RpcError;
286
287 fn try_from(value: proto::rpc::AccountStorageDetails) -> Result<Self, Self::Error> {
288 let header: AccountStorageHeader = value
289 .header
290 .ok_or(proto::account::AccountStorageHeader::missing_field(stringify!(header)))?
291 .decode_and_verify()?;
292 let map_details = value
293 .map_details
294 .into_iter()
295 .map(core::convert::TryInto::try_into)
296 .collect::<Result<Vec<AccountStorageMapDetails>, RpcError>>()?;
297
298 for map_detail in &map_details {
302 let StorageMapEntries::PartialMap { partial_smt, .. } = &map_detail.entries else {
303 continue;
304 };
305
306 let slot = header
307 .slots()
308 .find(|slot| *slot.name() == map_detail.slot_name)
309 .ok_or_else(|| {
310 RpcError::InvalidResponse(format!(
311 "partial storage map references slot '{}', which is absent from the \
312 storage header",
313 map_detail.slot_name,
314 ))
315 })?;
316 if slot.slot_type() != StorageSlotType::Map {
317 return Err(RpcError::InvalidResponse(format!(
318 "partial storage map references slot '{}', which is not a map",
319 map_detail.slot_name,
320 )));
321 }
322 if partial_smt.root() != slot.value() {
323 return Err(RpcError::InvalidResponse(format!(
324 "partial storage map for slot '{}' has root {} but the storage header reports \
325 {}",
326 map_detail.slot_name,
327 partial_smt.root(),
328 slot.value(),
329 )));
330 }
331 }
332
333 Ok(Self { header, map_details })
334 }
335}
336
337#[derive(Clone, Debug)]
341pub struct AccountStorageMapDetails {
342 pub slot_name: StorageSlotName,
344 pub entries: StorageMapEntries,
346}
347
348impl AccountStorageMapDetails {
349 pub const MAX_PARTIAL_MAP_KEYS: usize = 64;
352
353 pub fn is_limit_exceeded(&self) -> bool {
357 matches!(self.entries, StorageMapEntries::LimitExceeded)
358 }
359}
360
361impl TryFrom<proto::rpc::account_storage_details::AccountStorageMapDetails>
362 for AccountStorageMapDetails
363{
364 type Error = RpcError;
365
366 fn try_from(
367 value: proto::rpc::account_storage_details::AccountStorageMapDetails,
368 ) -> Result<Self, Self::Error> {
369 use proto::rpc::account_storage_details::account_storage_map_details::Result as ProtoResult;
370
371 let slot_name = StorageSlotName::new(value.slot_name)
372 .map_err(|err| RpcError::ExpectedDataMissing(err.to_string()))?;
373
374 let entries = match value.result {
375 Some(ProtoResult::TooManyEntries(true)) => StorageMapEntries::LimitExceeded,
376 Some(ProtoResult::TooManyEntries(false)) => {
377 return Err(RpcError::InvalidResponse(
378 "too_many_entries must be true when set".into(),
379 ));
380 },
381 Some(ProtoResult::AllEntries(all_entries)) => {
382 let entries = all_entries
383 .entries
384 .into_iter()
385 .map(core::convert::TryInto::try_into)
386 .collect::<Result<Vec<StorageMapEntry>, RpcError>>()?;
387 StorageMapEntries::AllEntries(entries)
388 },
389 Some(ProtoResult::PartialMap(partial_map)) => {
390 if partial_map.map_keys.len() > Self::MAX_PARTIAL_MAP_KEYS {
391 return Err(RpcError::InvalidResponse(format!(
392 "partial storage map for slot '{slot_name}' contains {} keys, exceeding \
393 the limit of {}",
394 partial_map.map_keys.len(),
395 Self::MAX_PARTIAL_MAP_KEYS,
396 )));
397 }
398
399 let map_keys = partial_map
400 .map_keys
401 .into_iter()
402 .map(|key| Word::try_from(key).map(StorageMapKey::new))
403 .collect::<Result<Vec<_>, _>>()?;
404 if let Some(key) = first_duplicate_key(&map_keys) {
405 return Err(RpcError::InvalidResponse(format!(
406 "partial storage map for slot '{slot_name}' repeats key {}",
407 key.to_hex(),
408 )));
409 }
410
411 let partial_smt: PartialSmt = partial_map
412 .partial_smt
413 .ok_or(proto::rpc::account_storage_details::account_storage_map_details::PartialStorageMap::missing_field(
414 stringify!(partial_smt),
415 ))?
416 .decode_and_verify()?;
417
418 for key in &map_keys {
421 partial_smt.get_value(&key.hash().as_word()).map_err(|_| {
422 RpcError::InvalidResponse(format!(
423 "partial storage map for slot '{slot_name}' does not track key {}",
424 key.to_hex(),
425 ))
426 })?;
427 }
428
429 StorageMapEntries::PartialMap { map_keys, partial_smt }
430 },
431 None => {
432 return Err(RpcError::InvalidResponse(format!(
433 "storage map details for slot '{slot_name}' carry no result",
434 )));
435 },
436 };
437
438 Ok(Self { slot_name, entries })
439 }
440}
441
442fn first_duplicate_key(keys: &[StorageMapKey]) -> Option<&StorageMapKey> {
447 keys.iter()
448 .enumerate()
449 .find_map(|(index, key)| keys[..index].contains(key).then_some(key))
450}
451
452#[derive(Clone, Debug)]
457pub struct StorageMapEntry {
458 pub key: StorageMapKey,
459 pub value: Word,
460}
461
462impl TryFrom<proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry>
463 for StorageMapEntry
464{
465 type Error = RpcError;
466
467 fn try_from(value: proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry) -> Result<Self, Self::Error> {
468 let key = Word::try_from(value.key.ok_or(RpcError::ExpectedDataMissing("key".into()))?)
469 .map(StorageMapKey::new)?;
470 let value = value.value.ok_or(RpcError::ExpectedDataMissing("value".into()))?.try_into()?;
471 Ok(Self { key, value })
472 }
473}
474
475#[derive(Clone, Debug)]
481pub enum StorageMapEntries {
482 LimitExceeded,
485 AllEntries(Vec<StorageMapEntry>),
487 PartialMap {
493 map_keys: Vec<StorageMapKey>,
495 partial_smt: PartialSmt,
497 },
498}
499
500impl StorageMapEntries {
501 pub fn into_storage_map(
506 self,
507 ) -> Option<Result<StorageMap, miden_protocol::errors::StorageMapError>> {
508 match self {
509 StorageMapEntries::AllEntries(entries) => {
510 Some(StorageMap::with_entries(entries.into_iter().map(|e| (e.key, e.value))))
511 },
512 StorageMapEntries::LimitExceeded | StorageMapEntries::PartialMap { .. } => None,
513 }
514 }
515}
516
517#[derive(Clone, Debug)]
521pub struct AccountVaultDetails {
522 pub too_many_assets: bool,
525 pub assets: Vec<Asset>,
527}
528
529impl TryFrom<proto::rpc::AccountVaultDetails> for AccountVaultDetails {
530 type Error = RpcError;
531
532 fn try_from(value: proto::rpc::AccountVaultDetails) -> Result<Self, Self::Error> {
533 let too_many_assets = value.too_many_assets;
534 let assets = value
535 .assets
536 .into_iter()
537 .map(DecodeMessageExt::decode_and_verify)
538 .collect::<Result<Vec<Asset>, _>>()?;
539
540 Ok(Self { too_many_assets, assets })
541 }
542}
543
544#[derive(Clone, Debug)]
549pub struct AccountProof {
550 account_witness: AccountWitness,
552 state_headers: Option<AccountDetails>,
554}
555
556impl AccountProof {
557 pub fn new(
559 account_witness: AccountWitness,
560 account_details: Option<AccountDetails>,
561 ) -> Result<Self, AccountProofError> {
562 if let Some(AccountDetails {
563 header: account_header,
564 storage_details: _,
565 code,
566 ..
567 }) = &account_details
568 {
569 if account_header.to_commitment() != account_witness.state_commitment() {
570 return Err(AccountProofError::InconsistentAccountCommitment);
571 }
572 if account_header.id() != account_witness.id() {
573 return Err(AccountProofError::InconsistentAccountId);
574 }
575 if code.commitment() != account_header.code_commitment() {
576 return Err(AccountProofError::InconsistentCodeCommitment);
577 }
578 }
579
580 Ok(Self {
581 account_witness,
582 state_headers: account_details,
583 })
584 }
585
586 pub fn account_id(&self) -> AccountId {
588 self.account_witness.id()
589 }
590
591 pub fn account_header(&self) -> Option<&AccountHeader> {
593 self.state_headers.as_ref().map(|account_details| &account_details.header)
594 }
595
596 pub fn storage_header(&self) -> Option<&AccountStorageHeader> {
598 self.state_headers
599 .as_ref()
600 .map(|account_details| &account_details.storage_details.header)
601 }
602
603 pub fn storage_details(&self) -> Option<&AccountStorageDetails> {
605 self.state_headers.as_ref().map(|d| &d.storage_details)
606 }
607
608 pub fn vault_details(&self) -> Option<&AccountVaultDetails> {
610 self.state_headers.as_ref().map(|d| &d.vault_details)
611 }
612
613 pub fn find_map_details(
615 &self,
616 slot_name: &StorageSlotName,
617 ) -> Option<&AccountStorageMapDetails> {
618 self.state_headers
619 .as_ref()
620 .and_then(|details| details.storage_details.find_map_details(slot_name))
621 }
622
623 pub fn account_code(&self) -> Option<&AccountCode> {
625 self.state_headers.as_ref().map(|headers| &headers.code)
626 }
627
628 pub fn code_commitment(&self) -> Option<Word> {
630 self.account_code().map(AccountCode::commitment)
631 }
632
633 pub fn account_commitment(&self) -> Word {
635 self.account_witness.state_commitment()
636 }
637
638 pub fn account_witness(&self) -> &AccountWitness {
639 &self.account_witness
640 }
641
642 pub fn merkle_proof(&self) -> &SparseMerklePath {
644 self.account_witness.path()
645 }
646
647 pub fn into_parts(self) -> (AccountWitness, Option<AccountDetails>) {
649 (self.account_witness, self.state_headers)
650 }
651
652 pub fn into_details(self) -> Option<AccountDetails> {
654 self.state_headers
655 }
656
657 pub fn details_mut(&mut self) -> Option<&mut AccountDetails> {
663 self.state_headers.as_mut()
664 }
665}
666
667#[cfg(feature = "tonic")]
668impl TryFrom<proto::rpc::GetAccountResponse> for AccountProof {
669 type Error = RpcError;
670 fn try_from(account_proof: proto::rpc::GetAccountResponse) -> Result<Self, Self::Error> {
671 let Some(witness) = account_proof.witness else {
672 return Err(RpcError::ExpectedDataMissing(
673 "GetAccount returned an account without witness".to_string(),
674 ));
675 };
676
677 let details: Option<AccountDetails> = {
678 match account_proof.details {
679 None => None,
680 Some(details) => Some(
681 details
682 .into_domain(&BTreeMap::new(), &AccountStorageRequirements::default())?,
683 ),
684 }
685 };
686 AccountProof::new(witness.decode_and_verify()?, details)
687 .map_err(|err| RpcError::InvalidResponse(format!("{err}")))
688 }
689}
690
691#[derive(Clone, Debug, Default, Eq, PartialEq)]
700pub struct AccountStorageRequirements(BTreeMap<StorageSlotName, Vec<StorageMapKey>>);
701
702impl AccountStorageRequirements {
703 pub fn new<'a>(
709 slots_and_keys: impl IntoIterator<
710 Item = (StorageSlotName, impl IntoIterator<Item = &'a StorageMapKey>),
711 >,
712 ) -> Self {
713 let map = slots_and_keys
714 .into_iter()
715 .map(|(slot_name, keys_iter)| {
716 let mut keys_vec: Vec<StorageMapKey> = Vec::new();
717 for key in keys_iter {
718 if !keys_vec.contains(key) {
719 keys_vec.push(*key);
720 }
721 }
722 (slot_name, keys_vec)
723 })
724 .collect();
725
726 AccountStorageRequirements(map)
727 }
728
729 pub fn all_entries(slot_names: &[StorageSlotName]) -> Self {
732 AccountStorageRequirements(
733 slot_names.iter().map(|name| (name.clone(), Vec::new())).collect(),
734 )
735 }
736
737 pub fn inner(&self) -> &BTreeMap<StorageSlotName, Vec<StorageMapKey>> {
738 &self.0
739 }
740
741 pub fn keys_for_slot(&self, slot_name: &StorageSlotName) -> &[StorageMapKey] {
743 self.0.get(slot_name).map_or(&[], Vec::as_slice)
744 }
745}
746
747impl From<AccountStorageRequirements> for Vec<StorageMapDetailRequest> {
748 fn from(value: AccountStorageRequirements) -> Vec<StorageMapDetailRequest> {
749 let request_map = value.0;
750 let mut requests = Vec::with_capacity(request_map.len());
751 for (slot_name, map_keys) in request_map {
752 let slot_data = if map_keys.is_empty() {
753 Some(SlotData::AllEntries(true))
754 } else {
755 let keys = map_keys.into_iter().map(|key| Word::from(key).into()).collect();
756 Some(SlotData::MapKeys(MapKeys { map_keys: keys }))
757 };
758 requests.push(StorageMapDetailRequest {
759 slot_name: slot_name.to_string(),
760 slot_data,
761 });
762 }
763 requests
764 }
765}
766
767impl Serializable for AccountStorageRequirements {
768 fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
769 target.write(&self.0);
770 }
771}
772
773impl Deserializable for AccountStorageRequirements {
774 fn read_from<R: miden_tx::utils::serde::ByteReader>(
775 source: &mut R,
776 ) -> Result<Self, miden_tx::utils::serde::DeserializationError> {
777 Ok(AccountStorageRequirements(source.read()?))
778 }
779}
780
781#[derive(Clone, Debug, Default)]
786pub enum VaultFetch {
787 #[default]
789 Skip,
790 Always,
792 IfChangedFrom(Word),
797}
798
799impl From<VaultFetch> for Option<proto::primitives::Word> {
800 fn from(vault: VaultFetch) -> Self {
804 match vault {
805 VaultFetch::Skip => None,
806 VaultFetch::Always => Some(EMPTY_WORD.into()),
807 VaultFetch::IfChangedFrom(commitment) => Some(commitment.into()),
808 }
809 }
810}
811
812#[derive(Clone, Debug, Default)]
818pub enum StorageMapFetch {
819 #[default]
821 Skip,
822 All,
826 Slots(AccountStorageRequirements),
829}
830
831impl From<StorageMapFetch> for Option<StorageRequest> {
832 fn from(storage: StorageMapFetch) -> Self {
833 match storage {
834 StorageMapFetch::Skip => None,
835 StorageMapFetch::All => Some(StorageRequest::AllStorageMaps(true)),
836 StorageMapFetch::Slots(reqs) => {
837 Some(StorageRequest::StorageMaps(StorageMapDetailRequests {
838 storage_maps: reqs.into(),
839 }))
840 },
841 }
842 }
843}
844
845#[derive(Clone, Debug, Default)]
847pub struct GetAccountRequest {
848 pub storage: StorageMapFetch,
850 pub at: AccountStateAt,
852 pub known_code: Option<AccountCode>,
855 pub vault: VaultFetch,
857}
858
859impl GetAccountRequest {
860 #[must_use]
864 pub fn new() -> Self {
865 Self {
866 storage: StorageMapFetch::Skip,
867 at: AccountStateAt::ChainTip,
868 known_code: None,
869 vault: VaultFetch::Skip,
870 }
871 }
872
873 #[must_use]
875 pub fn with_storage(mut self, storage: StorageMapFetch) -> Self {
876 self.storage = storage;
877 self
878 }
879
880 #[must_use]
882 pub fn at(mut self, at: AccountStateAt) -> Self {
883 self.at = at;
884 self
885 }
886
887 #[must_use]
890 pub fn with_known_code(mut self, known_code: Option<AccountCode>) -> Self {
891 self.known_code = known_code;
892 self
893 }
894
895 #[must_use]
897 pub fn with_vault(mut self, vault: VaultFetch) -> Self {
898 self.vault = vault;
899 self
900 }
901}
902
903#[derive(Debug, Error)]
907pub enum AccountProofError {
908 #[error(
909 "the received account commitment doesn't match the received account header's commitment"
910 )]
911 InconsistentAccountCommitment,
912 #[error("the received account id doesn't match the received account header's id")]
913 InconsistentAccountId,
914 #[error(
915 "the received code commitment doesn't match the received account header's code commitment"
916 )]
917 InconsistentCodeCommitment,
918}
919
920#[cfg(test)]
921mod tests {
922 use super::proto;
923 use crate::alloc::string::ToString;
924
925 #[test]
926 fn invitation_code_validation_request_debug_hides_code() {
927 let code = "secret-invitation-code";
928 let request =
929 proto::rpc::IsInvitationCodeValidRequest { invitation_code: code.to_string() };
930
931 let debug = format!("{request:?}");
932 assert!(debug.contains("IsInvitationCodeValidRequest"));
933 assert!(!debug.contains(code));
934 }
935}