use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt::Debug;
use miden_protocol::account::{
Account,
AccountCode,
AccountHeader,
AccountId,
AccountStorage,
StorageMapKey,
StorageMapWitness,
StorageSlot,
StorageSlotContent,
StorageSlotName,
};
use miden_protocol::address::Address;
use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey, AssetWitness};
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::crypto::merkle::mmr::{Forest, InOrderIndex, MmrPeaks, PartialMmr};
use miden_protocol::errors::AccountError;
use miden_protocol::note::{NoteId, NoteScript, NoteTag, Nullifier};
use miden_protocol::transaction::TransactionId;
use miden_protocol::{Felt, Word};
use miden_tx::utils::serde::{Deserializable, Serializable};
use crate::note_transport::{NOTE_TRANSPORT_CURSOR_STORE_SETTING, NoteTransportCursor};
use crate::rpc::{RPC_LIMITS_STORE_SETTING, RpcLimits};
use crate::sync::{NoteTagRecord, StateSyncUpdate};
use crate::transaction::{TransactionRecord, TransactionStatusVariant, TransactionStoreUpdate};
pub(crate) mod data_store;
mod errors;
pub use errors::*;
mod smt_forest;
pub use smt_forest::AccountSmtForest;
mod account;
pub use account::{AccountRecord, AccountRecordData, AccountStatus, AccountUpdates};
pub use crate::sync::PublicAccountUpdate;
mod note_record;
pub use note_record::{
InputNoteRecord,
InputNoteState,
NoteExportType,
NoteRecordError,
OutputNoteRecord,
OutputNoteState,
input_note_states,
};
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait Store: Send + Sync {
fn identifier(&self) -> &str;
fn get_current_timestamp(&self) -> Option<u64>;
async fn get_transactions(
&self,
filter: TransactionFilter,
) -> Result<Vec<TransactionRecord>, StoreError>;
async fn apply_transaction(&self, tx_update: TransactionStoreUpdate) -> Result<(), StoreError>;
async fn get_input_notes(&self, filter: NoteFilter)
-> Result<Vec<InputNoteRecord>, StoreError>;
async fn get_output_notes(
&self,
filter: NoteFilter,
) -> Result<Vec<OutputNoteRecord>, StoreError>;
async fn get_input_note_by_offset(
&self,
filter: NoteFilter,
consumer: AccountId,
block_start: Option<BlockNumber>,
block_end: Option<BlockNumber>,
offset: u32,
) -> Result<Option<InputNoteRecord>, StoreError>;
async fn get_unspent_input_note_nullifiers(&self) -> Result<Vec<Nullifier>, StoreError> {
self.get_input_notes(NoteFilter::Unspent)
.await?
.iter()
.map(|input_note| Ok(input_note.nullifier()))
.collect::<Result<Vec<_>, _>>()
}
async fn upsert_input_notes(&self, notes: &[InputNoteRecord]) -> Result<(), StoreError>;
async fn get_note_script(&self, script_root: Word) -> Result<NoteScript, StoreError>;
async fn upsert_note_scripts(&self, note_scripts: &[NoteScript]) -> Result<(), StoreError>;
async fn get_block_headers(
&self,
block_numbers: &BTreeSet<BlockNumber>,
) -> Result<Vec<(BlockHeader, BlockRelevance)>, StoreError>;
async fn get_block_header_by_num(
&self,
block_number: BlockNumber,
) -> Result<Option<(BlockHeader, BlockRelevance)>, StoreError> {
self.get_block_headers(&[block_number].into_iter().collect())
.await
.map(|mut block_headers_list| block_headers_list.pop())
}
async fn get_tracked_block_headers(&self) -> Result<Vec<BlockHeader>, StoreError>;
async fn get_tracked_block_header_numbers(&self) -> Result<BTreeSet<usize>, StoreError>;
async fn get_partial_blockchain_nodes(
&self,
filter: PartialBlockchainFilter,
) -> Result<BTreeMap<InOrderIndex, Word>, StoreError>;
async fn insert_partial_blockchain_nodes(
&self,
nodes: &[(InOrderIndex, Word)],
) -> Result<(), StoreError>;
async fn get_partial_blockchain_peaks_by_block_num(
&self,
block_num: BlockNumber,
) -> Result<MmrPeaks, StoreError>;
async fn insert_block_header(
&self,
block_header: &BlockHeader,
partial_blockchain_peaks: MmrPeaks,
has_client_notes: bool,
) -> Result<(), StoreError>;
async fn prune_irrelevant_blocks(&self) -> Result<(), StoreError>;
async fn prune_account_history(
&self,
account_id: AccountId,
up_to_nonce: Felt,
) -> Result<usize, StoreError>;
async fn get_account_ids(&self) -> Result<Vec<AccountId>, StoreError>;
async fn get_account_headers(&self) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError>;
async fn get_account_header(
&self,
account_id: AccountId,
) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError>;
async fn get_account_header_by_commitment(
&self,
account_commitment: Word,
) -> Result<Option<AccountHeader>, StoreError>;
async fn get_account(&self, account_id: AccountId)
-> Result<Option<AccountRecord>, StoreError>;
async fn get_account_code(
&self,
account_id: AccountId,
) -> Result<Option<AccountCode>, StoreError>;
async fn insert_account(
&self,
account: &Account,
initial_address: Address,
) -> Result<(), StoreError>;
async fn upsert_foreign_account_code(
&self,
account_id: AccountId,
code: AccountCode,
) -> Result<(), StoreError>;
async fn get_foreign_account_code(
&self,
account_ids: Vec<AccountId>,
) -> Result<BTreeMap<AccountId, AccountCode>, StoreError>;
async fn get_addresses_by_account_id(
&self,
account_id: AccountId,
) -> Result<Vec<Address>, StoreError>;
async fn update_account(&self, new_account_state: &Account) -> Result<(), StoreError>;
async fn insert_address(
&self,
address: Address,
account_id: AccountId,
) -> Result<(), StoreError>;
async fn remove_address(
&self,
address: Address,
account_id: AccountId,
) -> Result<(), StoreError>;
async fn set_setting(&self, key: String, value: Vec<u8>) -> Result<(), StoreError>;
async fn get_setting(&self, key: String) -> Result<Option<Vec<u8>>, StoreError>;
async fn remove_setting(&self, key: String) -> Result<(), StoreError>;
async fn list_setting_keys(&self) -> Result<Vec<String>, StoreError>;
async fn get_note_tags(&self) -> Result<Vec<NoteTagRecord>, StoreError>;
async fn get_unique_note_tags(&self) -> Result<BTreeSet<NoteTag>, StoreError> {
Ok(self.get_note_tags().await?.into_iter().map(|r| r.tag).collect())
}
async fn add_note_tag(&self, tag: NoteTagRecord) -> Result<bool, StoreError>;
async fn remove_note_tag(&self, tag: NoteTagRecord) -> Result<usize, StoreError>;
async fn get_sync_height(&self) -> Result<BlockNumber, StoreError>;
async fn apply_state_sync(&self, state_sync_update: StateSyncUpdate) -> Result<(), StoreError>;
async fn get_note_transport_cursor(&self) -> Result<NoteTransportCursor, StoreError> {
let cursor_bytes = if let Some(bytes) =
self.get_setting(NOTE_TRANSPORT_CURSOR_STORE_SETTING.into()).await?
{
bytes
} else {
let initial = 0u64.to_be_bytes().to_vec();
self.set_setting(NOTE_TRANSPORT_CURSOR_STORE_SETTING.into(), initial.clone())
.await?;
initial
};
let array: [u8; 8] = cursor_bytes
.as_slice()
.try_into()
.map_err(|e: core::array::TryFromSliceError| StoreError::ParsingError(e.to_string()))?;
let cursor = u64::from_be_bytes(array);
Ok(cursor.into())
}
async fn update_note_transport_cursor(
&self,
cursor: NoteTransportCursor,
) -> Result<(), StoreError> {
let cursor_bytes = cursor.value().to_be_bytes().to_vec();
self.set_setting(NOTE_TRANSPORT_CURSOR_STORE_SETTING.into(), cursor_bytes)
.await?;
Ok(())
}
async fn get_rpc_limits(&self) -> Result<Option<RpcLimits>, StoreError> {
let Some(bytes) = self.get_setting(RPC_LIMITS_STORE_SETTING.into()).await? else {
return Ok(None);
};
let limits = RpcLimits::read_from_bytes(&bytes)?;
Ok(Some(limits))
}
async fn set_rpc_limits(&self, limits: RpcLimits) -> Result<(), StoreError> {
self.set_setting(RPC_LIMITS_STORE_SETTING.into(), limits.to_bytes()).await
}
async fn get_current_partial_mmr(&self) -> Result<PartialMmr, StoreError> {
let current_block_num = self.get_sync_height().await?;
let current_peaks =
self.get_partial_blockchain_peaks_by_block_num(current_block_num).await?;
let (current_block, has_client_notes) = self
.get_block_header_by_num(current_block_num)
.await?
.ok_or(StoreError::BlockHeaderNotFound(current_block_num))?;
let mut current_partial_mmr = PartialMmr::from_peaks(current_peaks);
let has_client_notes = has_client_notes.into();
current_partial_mmr.add(current_block.commitment(), has_client_notes);
let mut tracked_leaves = self.get_tracked_block_header_numbers().await?;
if has_client_notes && current_partial_mmr.forest().has_single_leaf_tree() {
let latest_leaf = current_partial_mmr.forest().num_leaves().saturating_sub(1);
tracked_leaves.insert(latest_leaf);
}
let tracked_nodes = self
.get_partial_blockchain_nodes(PartialBlockchainFilter::Forest(
current_partial_mmr.forest(),
))
.await?;
let current_partial_mmr =
PartialMmr::from_parts(current_partial_mmr.peaks(), tracked_nodes, tracked_leaves)?;
Ok(current_partial_mmr)
}
async fn get_account_vault(&self, account_id: AccountId) -> Result<AssetVault, StoreError>;
async fn get_account_asset(
&self,
account_id: AccountId,
vault_key: AssetVaultKey,
) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
let vault = self.get_account_vault(account_id).await?;
let Some(asset) = vault.assets().find(|a| a.vault_key() == vault_key) else {
return Ok(None);
};
let witness = AssetWitness::new(vault.open(vault_key).into())?;
Ok(Some((asset, witness)))
}
async fn get_account_storage(
&self,
account_id: AccountId,
filter: AccountStorageFilter,
) -> Result<AccountStorage, StoreError>;
async fn get_account_storage_item(
&self,
account_id: AccountId,
slot_name: StorageSlotName,
) -> Result<Word, StoreError> {
let storage = self
.get_account_storage(account_id, AccountStorageFilter::SlotName(slot_name.clone()))
.await?;
storage
.get(&slot_name)
.map(StorageSlot::value)
.ok_or(StoreError::AccountError(AccountError::StorageSlotNameNotFound { slot_name }))
}
async fn get_account_map_item(
&self,
account_id: AccountId,
slot_name: StorageSlotName,
key: StorageMapKey,
) -> Result<(Word, StorageMapWitness), StoreError> {
let storage = self
.get_account_storage(account_id, AccountStorageFilter::SlotName(slot_name.clone()))
.await?;
match storage.get(&slot_name).map(StorageSlot::content) {
Some(StorageSlotContent::Map(map)) => {
let value = map.get(&key);
let witness = map.open(&key);
Ok((value, witness))
},
Some(_) => Err(StoreError::AccountError(AccountError::StorageSlotNotMap(slot_name))),
None => {
Err(StoreError::AccountError(AccountError::StorageSlotNameNotFound { slot_name }))
},
}
}
async fn get_minimal_partial_account(
&self,
account_id: AccountId,
) -> Result<Option<AccountRecord>, StoreError>;
}
pub enum PartialBlockchainFilter {
All,
List(Vec<InOrderIndex>),
Forest(Forest),
}
#[derive(Debug, Clone)]
pub enum TransactionFilter {
All,
Uncommitted,
Ids(Vec<TransactionId>),
ExpiredBefore(BlockNumber),
}
impl TransactionFilter {
pub fn to_query(&self) -> String {
const QUERY: &str = "SELECT tx.id, script.script, tx.details, tx.status \
FROM transactions AS tx LEFT JOIN transaction_scripts AS script ON tx.script_root = script.script_root";
match self {
TransactionFilter::All => QUERY.to_string(),
TransactionFilter::Uncommitted => format!(
"{QUERY} WHERE tx.status_variant = {}",
TransactionStatusVariant::Pending as u8,
),
TransactionFilter::Ids(_) => {
format!("{QUERY} WHERE tx.id IN rarray(?)")
},
TransactionFilter::ExpiredBefore(block_num) => {
format!(
"{QUERY} WHERE tx.block_num < {} AND tx.status_variant != {} AND tx.status_variant != {}",
block_num.as_u32(),
TransactionStatusVariant::Discarded as u8,
TransactionStatusVariant::Committed as u8
)
},
}
}
}
#[derive(Debug, Clone)]
pub enum NoteFilter {
All,
Committed,
Consumed,
Expected,
List(Vec<NoteId>),
Nullifiers(Vec<Nullifier>),
Processing,
Unique(NoteId),
Unspent,
Unverified,
}
#[derive(Debug, Clone)]
pub enum BlockRelevance {
HasNotes,
Irrelevant,
}
impl From<BlockRelevance> for bool {
fn from(val: BlockRelevance) -> Self {
match val {
BlockRelevance::HasNotes => true,
BlockRelevance::Irrelevant => false,
}
}
}
impl From<bool> for BlockRelevance {
fn from(has_notes: bool) -> Self {
if has_notes {
BlockRelevance::HasNotes
} else {
BlockRelevance::Irrelevant
}
}
}
#[derive(Debug, Clone)]
pub enum AccountStorageFilter {
All,
Root(Word),
SlotName(StorageSlotName),
}