use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::fmt::{self, Debug, Display, Formatter};
use miden_protocol::Word;
use miden_protocol::account::{
Account, AccountCode, AccountHeader, AccountId, AccountStorage, AccountStorageHeader,
StorageMap, StorageMapKey, StorageSlot, StorageSlotHeader, StorageSlotName, StorageSlotType,
};
use miden_protocol::asset::{Asset, AssetVault};
use miden_protocol::block::BlockNumber;
use miden_protocol::block::account_tree::AccountWitness;
use miden_protocol::crypto::merkle::SparseMerklePath;
use miden_protocol::crypto::merkle::smt::SmtProof;
use miden_tx::utils::ToHex;
use miden_tx::utils::serde::{Deserializable, Serializable};
use thiserror::Error;
use crate::alloc::string::ToString;
use crate::rpc::RpcError;
use crate::rpc::domain::MissingFieldHelper;
use crate::rpc::errors::RpcConversionError;
use crate::rpc::generated::rpc::account_request::account_detail_request::storage_map_detail_request::{MapKeys, SlotData};
use crate::rpc::generated::rpc::account_request::account_detail_request::StorageMapDetailRequest;
use crate::rpc::generated::{self as proto};
#[derive(Debug)]
pub enum FetchedAccount {
Private(AccountId, AccountUpdateSummary),
Public(Box<Account>, AccountUpdateSummary),
}
impl FetchedAccount {
pub fn new_private(account_id: AccountId, summary: AccountUpdateSummary) -> Self {
Self::Private(account_id, summary)
}
pub fn new_public(account: Account, summary: AccountUpdateSummary) -> Self {
Self::Public(Box::new(account), summary)
}
pub fn account_id(&self) -> AccountId {
match self {
Self::Private(account_id, _) => *account_id,
Self::Public(account, _) => account.id(),
}
}
pub fn commitment(&self) -> Word {
match self {
Self::Private(_, summary) | Self::Public(_, summary) => summary.commitment,
}
}
pub fn account(&self) -> Option<&Account> {
match self {
Self::Private(..) => None,
Self::Public(account, _) => Some(account.as_ref()),
}
}
}
impl From<FetchedAccount> for Option<Account> {
fn from(acc: FetchedAccount) -> Self {
match acc {
FetchedAccount::Private(..) => None,
FetchedAccount::Public(account, _) => Some(*account),
}
}
}
#[derive(Debug)]
pub struct AccountUpdateSummary {
pub commitment: Word,
pub last_block_num: BlockNumber,
}
impl AccountUpdateSummary {
pub fn new(commitment: Word, last_block_num: BlockNumber) -> Self {
Self { commitment, last_block_num }
}
}
impl Display for proto::account::AccountId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!("0x{}", self.id.to_hex()))
}
}
impl Debug for proto::account::AccountId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
impl From<AccountId> for proto::account::AccountId {
fn from(account_id: AccountId) -> Self {
Self { id: account_id.to_bytes() }
}
}
impl TryFrom<proto::account::AccountId> for AccountId {
type Error = RpcConversionError;
fn try_from(account_id: proto::account::AccountId) -> Result<Self, Self::Error> {
AccountId::read_from_bytes(&account_id.id).map_err(|_| RpcConversionError::NotAValidFelt)
}
}
impl TryInto<AccountHeader> for proto::account::AccountHeader {
type Error = crate::rpc::RpcError;
fn try_into(self) -> Result<AccountHeader, Self::Error> {
use miden_protocol::Felt;
use crate::rpc::domain::MissingFieldHelper;
let proto::account::AccountHeader {
account_id,
nonce,
vault_root,
storage_commitment,
code_commitment,
} = self;
let account_id: AccountId = account_id
.ok_or(proto::account::AccountHeader::missing_field(stringify!(account_id)))?
.try_into()?;
let vault_root = vault_root
.ok_or(proto::account::AccountHeader::missing_field(stringify!(vault_root)))?
.try_into()?;
let storage_commitment = storage_commitment
.ok_or(proto::account::AccountHeader::missing_field(stringify!(storage_commitment)))?
.try_into()?;
let code_commitment = code_commitment
.ok_or(proto::account::AccountHeader::missing_field(stringify!(code_commitment)))?
.try_into()?;
Ok(AccountHeader::new(
account_id,
Felt::new(nonce),
vault_root,
storage_commitment,
code_commitment,
))
}
}
impl TryInto<AccountStorageHeader> for proto::account::AccountStorageHeader {
type Error = crate::rpc::RpcError;
fn try_into(self) -> Result<AccountStorageHeader, Self::Error> {
use crate::rpc::RpcError;
use crate::rpc::domain::MissingFieldHelper;
let mut header_slots: Vec<StorageSlotHeader> = Vec::with_capacity(self.slots.len());
for slot in self.slots {
let slot_value: Word = slot
.commitment
.ok_or(proto::account::account_storage_header::StorageSlot::missing_field(
stringify!(commitment),
))?
.try_into()?;
let slot_type = u8::try_from(slot.slot_type)
.map_err(|e| RpcError::InvalidResponse(e.to_string()))
.and_then(|v| {
StorageSlotType::try_from(v)
.map_err(|e| RpcError::InvalidResponse(e.to_string()))
})?;
let slot_name = StorageSlotName::new(slot.slot_name)
.map_err(|err| RpcError::InvalidResponse(err.to_string()))?;
header_slots.push(StorageSlotHeader::new(slot_name, slot_type, slot_value));
}
header_slots.sort_by_key(StorageSlotHeader::id);
AccountStorageHeader::new(header_slots)
.map_err(|err| RpcError::InvalidResponse(err.to_string()))
}
}
#[cfg(feature = "tonic")]
impl proto::rpc::account_response::AccountDetails {
pub fn into_domain(
self,
known_account_codes: &BTreeMap<Word, AccountCode>,
storage_requirements: &AccountStorageRequirements,
) -> Result<AccountDetails, crate::rpc::RpcError> {
use crate::rpc::RpcError;
use crate::rpc::domain::MissingFieldHelper;
let proto::rpc::account_response::AccountDetails {
header,
storage_details,
code,
vault_details,
} = self;
let header: AccountHeader = header
.ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!(header)))?
.try_into()?;
let storage_details: AccountStorageDetails = storage_details
.ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!(
storage_details
)))?
.try_into()?;
for map_detail in &storage_details.map_details {
let requested_keys = storage_requirements
.inner()
.get(&map_detail.slot_name)
.map(Vec::as_slice)
.unwrap_or_default();
if let StorageMapEntries::EntriesWithProofs(proofs) = &map_detail.entries {
if proofs.len() != requested_keys.len() {
return Err(RpcError::InvalidResponse(format!(
"expected {} proofs for storage map slot '{}', got {}",
requested_keys.len(),
map_detail.slot_name,
proofs.len(),
)));
}
for (proof, raw_key) in proofs.iter().zip(requested_keys.iter()) {
let hashed_key = raw_key.hash().as_word();
if proof.get(&hashed_key).is_none() {
return Err(RpcError::InvalidResponse(format!(
"proof for storage map key {} does not match the requested key",
raw_key.to_hex(),
)));
}
}
}
}
let code = {
let received_code = code.map(|c| AccountCode::read_from_bytes(&c)).transpose()?;
match received_code {
Some(code) => code,
None => known_account_codes
.get(&header.code_commitment())
.ok_or(RpcError::InvalidResponse(
"Account code was not provided, but the response did not contain it either"
.into(),
))?
.clone(),
}
};
let vault_details = vault_details
.ok_or(proto::rpc::AccountVaultDetails::missing_field(stringify!(vault_details)))?
.try_into()?;
Ok(AccountDetails {
header,
storage_details,
code,
vault_details,
})
}
}
pub type AccountProofs = (BlockNumber, Vec<AccountProof>);
#[derive(Clone, Debug)]
pub struct AccountDetails {
pub header: AccountHeader,
pub storage_details: AccountStorageDetails,
pub code: AccountCode,
pub vault_details: AccountVaultDetails,
}
impl TryFrom<&AccountDetails> for Account {
type Error = RpcError;
fn try_from(details: &AccountDetails) -> Result<Self, Self::Error> {
if details.vault_details.too_many_assets {
return Err(RpcError::ExpectedDataMissing(
"cannot build account: vault has too many assets".into(),
));
}
if let Some(slot_name) = details
.storage_details
.map_details
.iter()
.find(|m| m.too_many_entries)
.map(|m| &m.slot_name)
{
return Err(RpcError::ExpectedDataMissing(format!(
"cannot build account: storage map slot '{slot_name}' has too many entries",
)));
}
let mut slots: Vec<StorageSlot> = Vec::new();
for slot_header in details.storage_details.header.slots() {
match slot_header.slot_type() {
StorageSlotType::Value => {
slots.push(StorageSlot::with_value(
slot_header.name().clone(),
slot_header.value(),
));
},
StorageSlotType::Map => {
let map_details = details
.storage_details
.find_map_details(slot_header.name())
.ok_or_else(|| {
RpcError::ExpectedDataMissing(format!(
"slot '{}' is a map but has no map_details in response",
slot_header.name()
))
})?;
let storage_map = map_details
.entries
.clone()
.into_storage_map()
.ok_or_else(|| {
RpcError::ExpectedDataMissing(
"expected AllEntries for full account fetch, got EntriesWithProofs"
.into(),
)
})?
.map_err(|err| {
RpcError::InvalidResponse(format!(
"the rpc api returned a non-valid map entry: {err}"
))
})?;
slots.push(StorageSlot::with_map(slot_header.name().clone(), storage_map));
},
}
}
let asset_vault = AssetVault::new(&details.vault_details.assets).map_err(|err| {
RpcError::InvalidResponse(format!("rpc api returned non-valid assets: {err}"))
})?;
let account_storage = AccountStorage::new(slots).map_err(|err| {
RpcError::InvalidResponse(format!("rpc api returned non-valid storage slots: {err}"))
})?;
Account::new(
details.header.id(),
asset_vault,
account_storage,
details.code.clone(),
details.header.nonce(),
None,
)
.map_err(|err| {
RpcError::InvalidResponse(format!(
"failed to construct account from rpc api response: {err}"
))
})
}
}
#[derive(Clone, Debug)]
pub struct AccountStorageDetails {
pub header: AccountStorageHeader,
pub map_details: Vec<AccountStorageMapDetails>,
}
impl AccountStorageDetails {
pub fn find_map_details(&self, target: &StorageSlotName) -> Option<&AccountStorageMapDetails> {
self.map_details.iter().find(|map_detail| map_detail.slot_name == *target)
}
}
impl TryFrom<proto::rpc::AccountStorageDetails> for AccountStorageDetails {
type Error = RpcError;
fn try_from(value: proto::rpc::AccountStorageDetails) -> Result<Self, Self::Error> {
let header = value
.header
.ok_or(proto::account::AccountStorageHeader::missing_field(stringify!(header)))?
.try_into()?;
let map_details = value
.map_details
.into_iter()
.map(core::convert::TryInto::try_into)
.collect::<Result<Vec<AccountStorageMapDetails>, RpcError>>()?;
Ok(Self { header, map_details })
}
}
#[derive(Clone, Debug)]
pub struct AccountStorageMapDetails {
pub slot_name: StorageSlotName,
pub too_many_entries: bool,
pub entries: StorageMapEntries,
}
impl TryFrom<proto::rpc::account_storage_details::AccountStorageMapDetails>
for AccountStorageMapDetails
{
type Error = RpcError;
fn try_from(
value: proto::rpc::account_storage_details::AccountStorageMapDetails,
) -> Result<Self, Self::Error> {
use proto::rpc::account_storage_details::account_storage_map_details::Entries;
let slot_name = StorageSlotName::new(value.slot_name)
.map_err(|err| RpcError::ExpectedDataMissing(err.to_string()))?;
let too_many_entries = value.too_many_entries;
let entries = match value.entries {
Some(Entries::AllEntries(all_entries)) => {
let entries = all_entries
.entries
.into_iter()
.map(core::convert::TryInto::try_into)
.collect::<Result<Vec<StorageMapEntry>, RpcError>>()?;
StorageMapEntries::AllEntries(entries)
},
Some(Entries::EntriesWithProofs(entries_with_proofs)) => {
let proofs = entries_with_proofs
.entries
.into_iter()
.map(|entry| {
let proof: SmtProof = entry
.proof
.ok_or(RpcError::ExpectedDataMissing("proof".into()))?
.try_into()?;
Ok(proof)
})
.collect::<Result<Vec<SmtProof>, RpcError>>()?;
StorageMapEntries::EntriesWithProofs(proofs)
},
None => StorageMapEntries::AllEntries(Vec::new()),
};
Ok(Self { slot_name, too_many_entries, entries })
}
}
#[derive(Clone, Debug)]
pub struct StorageMapEntry {
pub key: StorageMapKey,
pub value: Word,
}
impl TryFrom<proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry>
for StorageMapEntry
{
type Error = RpcError;
fn try_from(value: proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry) -> Result<Self, Self::Error> {
let key: StorageMapKey =
value.key.ok_or(RpcError::ExpectedDataMissing("key".into()))?.try_into()?;
let value = value.value.ok_or(RpcError::ExpectedDataMissing("value".into()))?.try_into()?;
Ok(Self { key, value })
}
}
#[derive(Clone, Debug)]
pub enum StorageMapEntries {
AllEntries(Vec<StorageMapEntry>),
EntriesWithProofs(Vec<SmtProof>),
}
impl StorageMapEntries {
pub fn into_storage_map(
self,
) -> Option<Result<StorageMap, miden_protocol::errors::StorageMapError>> {
match self {
StorageMapEntries::AllEntries(entries) => {
Some(StorageMap::with_entries(entries.into_iter().map(|e| (e.key, e.value))))
},
StorageMapEntries::EntriesWithProofs(_) => None,
}
}
}
#[derive(Clone, Debug)]
pub struct AccountVaultDetails {
pub too_many_assets: bool,
pub assets: Vec<Asset>,
}
impl TryFrom<proto::rpc::AccountVaultDetails> for AccountVaultDetails {
type Error = RpcError;
fn try_from(value: proto::rpc::AccountVaultDetails) -> Result<Self, Self::Error> {
let too_many_assets = value.too_many_assets;
let assets = value
.assets
.into_iter()
.map(Asset::try_from)
.collect::<Result<Vec<Asset>, _>>()?;
Ok(Self { too_many_assets, assets })
}
}
#[derive(Clone, Debug)]
pub struct AccountProof {
account_witness: AccountWitness,
state_headers: Option<AccountDetails>,
}
impl AccountProof {
pub fn new(
account_witness: AccountWitness,
account_details: Option<AccountDetails>,
) -> Result<Self, AccountProofError> {
if let Some(AccountDetails {
header: account_header,
storage_details: _,
code,
..
}) = &account_details
{
if account_header.to_commitment() != account_witness.state_commitment() {
return Err(AccountProofError::InconsistentAccountCommitment);
}
if account_header.id() != account_witness.id() {
return Err(AccountProofError::InconsistentAccountId);
}
if code.commitment() != account_header.code_commitment() {
return Err(AccountProofError::InconsistentCodeCommitment);
}
}
Ok(Self {
account_witness,
state_headers: account_details,
})
}
pub fn account_id(&self) -> AccountId {
self.account_witness.id()
}
pub fn account_header(&self) -> Option<&AccountHeader> {
self.state_headers.as_ref().map(|account_details| &account_details.header)
}
pub fn storage_header(&self) -> Option<&AccountStorageHeader> {
self.state_headers
.as_ref()
.map(|account_details| &account_details.storage_details.header)
}
pub fn storage_details(&self) -> Option<&AccountStorageDetails> {
self.state_headers.as_ref().map(|d| &d.storage_details)
}
pub fn find_map_details(
&self,
slot_name: &StorageSlotName,
) -> Option<&AccountStorageMapDetails> {
self.state_headers
.as_ref()
.and_then(|details| details.storage_details.find_map_details(slot_name))
}
pub fn account_code(&self) -> Option<&AccountCode> {
self.state_headers.as_ref().map(|headers| &headers.code)
}
pub fn code_commitment(&self) -> Option<Word> {
self.account_code().map(AccountCode::commitment)
}
pub fn account_commitment(&self) -> Word {
self.account_witness.state_commitment()
}
pub fn account_witness(&self) -> &AccountWitness {
&self.account_witness
}
pub fn merkle_proof(&self) -> &SparseMerklePath {
self.account_witness.path()
}
pub fn into_parts(self) -> (AccountWitness, Option<AccountDetails>) {
(self.account_witness, self.state_headers)
}
}
#[cfg(feature = "tonic")]
impl TryFrom<proto::rpc::AccountResponse> for AccountProof {
type Error = RpcError;
fn try_from(account_proof: proto::rpc::AccountResponse) -> Result<Self, Self::Error> {
let Some(witness) = account_proof.witness else {
return Err(RpcError::ExpectedDataMissing(
"GetAccountProof returned an account without witness".to_string(),
));
};
let details: Option<AccountDetails> = {
match account_proof.details {
None => None,
Some(details) => Some(
details
.into_domain(&BTreeMap::new(), &AccountStorageRequirements::default())?,
),
}
};
AccountProof::new(witness.try_into()?, details)
.map_err(|err| RpcError::InvalidResponse(format!("{err}")))
}
}
impl TryFrom<proto::account::AccountWitness> for AccountWitness {
type Error = RpcError;
fn try_from(account_witness: proto::account::AccountWitness) -> Result<Self, Self::Error> {
let state_commitment = account_witness
.commitment
.ok_or(proto::account::AccountWitness::missing_field(stringify!(state_commitment)))?
.try_into()?;
let merkle_path = account_witness
.path
.ok_or(proto::account::AccountWitness::missing_field(stringify!(merkle_path)))?
.try_into()?;
let account_id = account_witness
.witness_id
.ok_or(proto::account::AccountWitness::missing_field(stringify!(witness_id)))?
.try_into()?;
let witness = AccountWitness::new(account_id, state_commitment, merkle_path).unwrap();
Ok(witness)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AccountStorageRequirements(BTreeMap<StorageSlotName, Vec<StorageMapKey>>);
impl AccountStorageRequirements {
pub fn new<'a>(
slots_and_keys: impl IntoIterator<
Item = (StorageSlotName, impl IntoIterator<Item = &'a StorageMapKey>),
>,
) -> Self {
let map = slots_and_keys
.into_iter()
.map(|(slot_name, keys_iter)| {
let keys_vec: Vec<StorageMapKey> = keys_iter.into_iter().copied().collect();
(slot_name, keys_vec)
})
.collect();
AccountStorageRequirements(map)
}
pub fn inner(&self) -> &BTreeMap<StorageSlotName, Vec<StorageMapKey>> {
&self.0
}
pub fn keys_for_slot(&self, slot_name: &StorageSlotName) -> &[StorageMapKey] {
self.0.get(slot_name).map_or(&[], Vec::as_slice)
}
}
impl From<AccountStorageRequirements> for Vec<StorageMapDetailRequest> {
fn from(value: AccountStorageRequirements) -> Vec<StorageMapDetailRequest> {
let request_map = value.0;
let mut requests = Vec::with_capacity(request_map.len());
for (slot_name, map_keys) in request_map {
let slot_data = if map_keys.is_empty() {
Some(SlotData::AllEntries(true))
} else {
let keys = map_keys.into_iter().map(|key| Word::from(key).into()).collect();
Some(SlotData::MapKeys(MapKeys { map_keys: keys }))
};
requests.push(StorageMapDetailRequest {
slot_name: slot_name.to_string(),
slot_data,
});
}
requests
}
}
impl Serializable for AccountStorageRequirements {
fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
target.write(&self.0);
}
}
impl Deserializable for AccountStorageRequirements {
fn read_from<R: miden_tx::utils::serde::ByteReader>(
source: &mut R,
) -> Result<Self, miden_tx::utils::serde::DeserializationError> {
Ok(AccountStorageRequirements(source.read()?))
}
}
#[derive(Debug, Error)]
pub enum AccountProofError {
#[error(
"the received account commitment doesn't match the received account header's commitment"
)]
InconsistentAccountCommitment,
#[error("the received account id doesn't match the received account header's id")]
InconsistentAccountId,
#[error(
"the received code commitment doesn't match the received account header's code commitment"
)]
InconsistentCodeCommitment,
}