miden_client/store/mod.rs
1//! Defines the storage interfaces used by the Miden client.
2//!
3//! It provides mechanisms for persisting and retrieving data, such as account states, transaction
4//! history, block headers, notes, and MMR nodes.
5//!
6//! ## Overview
7//!
8//! The storage module is central to the Miden client’s persistence layer. It defines the
9//! [`Store`] trait which abstracts over any concrete storage implementation. The trait exposes
10//! methods to (among others):
11//!
12//! - Retrieve and update transactions, notes, and accounts.
13//! - Store and query block headers along with MMR peaks and authentication nodes.
14//! - Manage note tags for synchronizing with the node.
15//!
16//! These are all used by the Miden client to provide transaction execution in the correct contexts.
17//!
18//! In addition to the main [`Store`] trait, the module provides types for filtering queries, such
19//! as [`TransactionFilter`], [`NoteFilter`], `StorageFilter` to narrow down the set of returned
20//! transactions, account data, or notes. For more advanced usage, see the documentation of
21//! individual methods in the [`Store`] trait.
22
23use alloc::boxed::Box;
24use alloc::collections::{BTreeMap, BTreeSet};
25use alloc::string::{String, ToString};
26use alloc::vec::Vec;
27use core::fmt::Debug;
28
29use miden_protocol::account::{
30 Account,
31 AccountCode,
32 AccountHeader,
33 AccountId,
34 AccountStorage,
35 StorageMapKey,
36 StorageMapWitness,
37 StorageSlot,
38 StorageSlotContent,
39 StorageSlotName,
40};
41use miden_protocol::address::Address;
42use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey, AssetWitness};
43use miden_protocol::block::{BlockHeader, BlockNumber};
44use miden_protocol::crypto::merkle::mmr::{Forest, InOrderIndex, MmrPeaks, PartialMmr};
45use miden_protocol::errors::AccountError;
46use miden_protocol::note::{NoteDetailsCommitment, NoteId, NoteScript, NoteTag, Nullifier};
47use miden_protocol::transaction::TransactionId;
48use miden_protocol::{Felt, Word};
49use miden_tx::utils::serde::{Deserializable, Serializable};
50
51use crate::note_transport::{NOTE_TRANSPORT_CURSOR_STORE_SETTING, NoteTransportCursor};
52use crate::rpc::{RPC_LIMITS_STORE_SETTING, RpcLimits};
53use crate::sync::{NoteTagRecord, StateSyncUpdate};
54use crate::transaction::{TransactionRecord, TransactionStatusVariant, TransactionStoreUpdate};
55
56/// Contains [`ClientDataStore`] to automatically implement [`DataStore`] for anything that
57/// implements [`Store`]. This isn't public because it's an implementation detail to instantiate the
58/// executor.
59///
60/// The user is tasked with creating a [`Store`] which the client will wrap into a
61/// [`ClientDataStore`] at creation time.
62pub(crate) mod data_store;
63
64mod errors;
65pub use errors::*;
66
67mod smt_forest;
68pub use smt_forest::AccountSmtForest;
69
70mod account;
71pub use account::{
72 AccountRecord,
73 AccountRecordData,
74 AccountStatus,
75 AccountUpdates,
76 ClientAccountType,
77};
78
79pub use crate::sync::PublicAccountUpdate;
80mod note_record;
81pub use note_record::{
82 InputNoteRecord,
83 InputNoteState,
84 NoteExportType,
85 NoteRecordError,
86 OutputNoteRecord,
87 OutputNoteState,
88 input_note_states,
89};
90
91// SETTING MUTATION
92// ================================================================================================
93
94/// A single mutation against the `settings` KV store, applied as part of an atomic batch via
95/// [`Store::apply_settings_mutations`].
96#[derive(Debug, Clone)]
97pub enum SettingMutation {
98 /// Insert or overwrite `key` with `value`.
99 Set { key: String, value: Vec<u8> },
100 /// Delete `key`.
101 Remove { key: String },
102}
103
104// STORE TRAIT
105// ================================================================================================
106
107/// The [`Store`] trait exposes all methods that the client store needs in order to track the
108/// current state.
109///
110/// All update functions are implied to be atomic. That is, if multiple entities are meant to be
111/// updated as part of any single function and an error is returned during its execution, any
112/// changes that might have happened up to that point need to be rolled back and discarded.
113///
114/// Because the [`Store`]'s ownership is shared between the executor and the client, interior
115/// mutability is expected to be implemented, which is why all methods receive `&self` and
116/// not `&mut self`.
117#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
118#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
119pub trait Store: Send + Sync {
120 /// Returns an identifier for this store (e.g. `IndexedDB` database name, `SQLite` file path).
121 ///
122 /// This allows callers to retrieve store-specific identity information (such as the `IndexedDB`
123 /// database name) for standalone operations like `exportStore`/`importStore`, without making
124 /// import/export a responsibility of the client.
125 fn identifier(&self) -> &str;
126
127 /// Returns the current timestamp tracked by the store, measured in non-leap seconds since
128 /// Unix epoch. If the store implementation is incapable of tracking time, it should return
129 /// `None`.
130 ///
131 /// This method is used to add time metadata to notes' states. This information doesn't have a
132 /// functional impact on the client's operation, it's shown to the user for informational
133 /// purposes.
134 fn get_current_timestamp(&self) -> Option<u64>;
135
136 // TRANSACTIONS
137 // --------------------------------------------------------------------------------------------
138
139 /// Retrieves stored transactions, filtered by [`TransactionFilter`].
140 async fn get_transactions(
141 &self,
142 filter: TransactionFilter,
143 ) -> Result<Vec<TransactionRecord>, StoreError>;
144
145 /// Applies a transaction, atomically updating the current state based on the
146 /// [`TransactionStoreUpdate`].
147 ///
148 /// An update involves:
149 /// - Updating the stored account which is being modified by the transaction.
150 /// - Storing new input/output notes and payback note details as a result of the transaction
151 /// execution.
152 /// - Updating the input notes that are being processed by the transaction.
153 /// - Inserting the new tracked tags into the store.
154 /// - Inserting the transaction into the store to track.
155 async fn apply_transaction(&self, tx_update: TransactionStoreUpdate) -> Result<(), StoreError>;
156
157 /// Applies a batch of [`TransactionStoreUpdate`]s atomically. Semantically equivalent to
158 /// calling [`Store::apply_transaction`] for each update in order, but with an all-or-nothing
159 /// guarantee — on any error no update is visible.
160 ///
161 /// Used by `BatchBuilder::submit` to persist a batch's results. Backends that cannot provide
162 /// true atomicity must document that limitation explicitly in their impl — there is no blanket
163 /// default.
164 async fn apply_transaction_batch(
165 &self,
166 tx_updates: Vec<TransactionStoreUpdate>,
167 ) -> Result<(), StoreError>;
168
169 // NOTES
170 // --------------------------------------------------------------------------------------------
171
172 /// Retrieves the input notes from the store.
173 ///
174 /// When `filter` is [`NoteFilter::Consumed`], notes are sorted by their on-chain execution
175 /// order.
176 async fn get_input_notes(&self, filter: NoteFilter)
177 -> Result<Vec<InputNoteRecord>, StoreError>;
178
179 /// Retrieves the output notes from the store.
180 async fn get_output_notes(
181 &self,
182 filter: NoteFilter,
183 ) -> Result<Vec<OutputNoteRecord>, StoreError>;
184
185 /// Retrieves a single input note at the given offset from the filtered set for the given
186 /// consumer account. Optionally restricts to a block range via `block_start` and
187 /// `block_end`. Returns `None` when the offset is past the end of the matching notes.
188 ///
189 /// # Ordering
190 ///
191 /// Notes are sorted by their per-account on-chain execution order.
192 async fn get_input_note_by_offset(
193 &self,
194 filter: NoteFilter,
195 consumer: AccountId,
196 block_start: Option<BlockNumber>,
197 block_end: Option<BlockNumber>,
198 offset: u32,
199 ) -> Result<Option<InputNoteRecord>, StoreError>;
200
201 /// Returns the nullifiers of all unspent input notes.
202 ///
203 /// The default implementation of this method uses [`Store::get_input_notes`].
204 async fn get_unspent_input_note_nullifiers(&self) -> Result<Vec<Nullifier>, StoreError> {
205 Ok(self
206 .get_input_notes(NoteFilter::Unspent)
207 .await?
208 .iter()
209 .filter_map(InputNoteRecord::nullifier)
210 .collect())
211 }
212
213 /// Inserts the provided input notes into the database. If a note with the same ID already
214 /// exists, it will be replaced.
215 async fn upsert_input_notes(&self, notes: &[InputNoteRecord]) -> Result<(), StoreError>;
216
217 /// Returns the note script associated with the given root.
218 async fn get_note_script(&self, script_root: Word) -> Result<NoteScript, StoreError>;
219
220 /// Inserts the provided note scripts into the database. If a script with the same root already
221 /// exists, it will be replaced.
222 async fn upsert_note_scripts(&self, note_scripts: &[NoteScript]) -> Result<(), StoreError>;
223
224 // CHAIN DATA
225 // --------------------------------------------------------------------------------------------
226
227 /// Retrieves a vector of [`BlockHeader`]s filtered by the provided block numbers.
228 ///
229 /// The returned vector may not contain some or all of the requested block headers. It's up to
230 /// the callee to check whether all requested block headers were found.
231 ///
232 /// For each block header an additional boolean value is returned representing whether the block
233 /// contains notes relevant to the client.
234 async fn get_block_headers(
235 &self,
236 block_numbers: &BTreeSet<BlockNumber>,
237 ) -> Result<Vec<(BlockHeader, BlockRelevance)>, StoreError>;
238
239 /// Retrieves a [`BlockHeader`] corresponding to the provided block number and a boolean value
240 /// that represents whether the block contains notes relevant to the client. Returns `None` if
241 /// the block is not found.
242 ///
243 /// The default implementation of this method uses [`Store::get_block_headers`].
244 async fn get_block_header_by_num(
245 &self,
246 block_number: BlockNumber,
247 ) -> Result<Option<(BlockHeader, BlockRelevance)>, StoreError> {
248 self.get_block_headers(&[block_number].into_iter().collect())
249 .await
250 .map(|mut block_headers_list| block_headers_list.pop())
251 }
252
253 /// Retrieves a list of [`BlockHeader`] that include relevant notes to the client.
254 async fn get_tracked_block_headers(&self) -> Result<Vec<BlockHeader>, StoreError>;
255
256 /// Retrieves the block numbers of block headers that include relevant notes to the client.
257 ///
258 /// This is a lightweight alternative to [`Store::get_tracked_block_headers`] that avoids
259 /// deserializing full block headers when only the block numbers are needed.
260 async fn get_tracked_block_header_numbers(&self) -> Result<BTreeSet<usize>, StoreError>;
261
262 /// Retrieves all MMR authentication nodes based on [`PartialBlockchainFilter`].
263 async fn get_partial_blockchain_nodes(
264 &self,
265 filter: PartialBlockchainFilter,
266 ) -> Result<BTreeMap<InOrderIndex, Word>, StoreError>;
267
268 /// Inserts blockchain MMR authentication nodes.
269 ///
270 /// In the case where the [`InOrderIndex`] already exists on the table, the insertion is
271 /// ignored.
272 async fn insert_partial_blockchain_nodes(
273 &self,
274 nodes: &[(InOrderIndex, Word)],
275 ) -> Result<(), StoreError>;
276
277 /// Returns the chain MMR peaks at the current sync height (peaks at `forest = block_num`,
278 /// i.e. excluding `block_num` itself as a leaf).
279 ///
280 /// The peaks' `forest().num_leaves()` equals the current sync height by construction,
281 /// so callers can derive the synced block number from the returned peaks without a
282 /// second query.
283 ///
284 /// Before the first sync, returns an empty [`MmrPeaks`].
285 async fn get_current_blockchain_peaks(&self) -> Result<MmrPeaks, StoreError>;
286
287 /// Inserts a block header into the store.
288 ///
289 /// Insert-if-not-exists with a one-way flag upgrade: on conflict with an existing row,
290 /// `header` is preserved and `has_client_notes` is only upgraded from `false` to `true`;
291 /// never downgraded.
292 // TODO: this method's name only tells half the story. The insert is conditional and
293 // the flag has its own upgrade rule. Revisit the name in a follow-up.
294 async fn insert_block_header(
295 &self,
296 block_header: &BlockHeader,
297 has_client_notes: bool,
298 ) -> Result<(), StoreError>;
299
300 /// Prunes irrelevant block data from the store.
301 ///
302 /// This performs three operations atomically:
303 /// 1. Deletes MMR authentication nodes at the given `node_indices`.
304 /// 2. Sets `has_client_notes = false` for `blocks_to_untrack` (blocks whose notes have all been
305 /// consumed).
306 /// 3. Deletes block headers with `has_client_notes = false` that are not the genesis or
307 /// sync-height block.
308 async fn untrack_and_prune_irrelevant_blocks(
309 &self,
310 blocks_to_untrack: &[BlockNumber],
311 node_indices_to_remove: &[InOrderIndex],
312 ) -> Result<(), StoreError>;
313
314 /// Prunes historical account states for the specified account up to the given nonce.
315 ///
316 /// Deletes all historical entries with `replaced_at_nonce <= up_to_nonce` from the
317 /// historical tables (headers, storage, storage map entries, and assets).
318 ///
319 /// Also removes orphaned `account_code` entries that are no longer referenced by any
320 /// account header.
321 ///
322 /// Returns the total number of rows deleted, including historical entries and orphaned
323 /// account code.
324 async fn prune_account_history(
325 &self,
326 account_id: AccountId,
327 up_to_nonce: Felt,
328 ) -> Result<usize, StoreError>;
329
330 // ACCOUNT
331 // --------------------------------------------------------------------------------------------
332
333 /// Returns the account IDs of all accounts stored in the database.
334 async fn get_account_ids(&self) -> Result<Vec<AccountId>, StoreError>;
335
336 /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
337 /// statuses.
338 ///
339 /// Said accounts' state is the state after the last performed sync.
340 async fn get_account_headers(&self) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError>;
341
342 /// Retrieves an [`AccountHeader`] object for the specified [`AccountId`] along with its status.
343 /// Returns `None` if the account is not found.
344 ///
345 /// Said account's state is the state according to the last sync performed.
346 async fn get_account_header(
347 &self,
348 account_id: AccountId,
349 ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError>;
350
351 /// Returns an [`AccountHeader`] corresponding to the stored account state that matches the
352 /// given commitment. If no account state matches the provided commitment, `None` is returned.
353 async fn get_account_header_by_commitment(
354 &self,
355 account_commitment: Word,
356 ) -> Result<Option<AccountHeader>, StoreError>;
357
358 /// Retrieves a full [`AccountRecord`] object, this contains the account's latest state along
359 /// with its status. Returns `None` if the account is not found.
360 async fn get_account(&self, account_id: AccountId)
361 -> Result<Option<AccountRecord>, StoreError>;
362
363 /// Retrieves the [`AccountCode`] for the specified account.
364 /// Returns `None` if the account is not found.
365 async fn get_account_code(
366 &self,
367 account_id: AccountId,
368 ) -> Result<Option<AccountCode>, StoreError>;
369
370 /// Inserts an [`Account`] to the store, alongside its initial [`Address`].
371 ///
372 /// Tag registration is the caller's responsibility — see [`Self::add_note_tag`].
373 ///
374 /// # Errors
375 ///
376 /// - If the account is new and does not contain a seed
377 async fn insert_account(
378 &self,
379 account: &Account,
380 initial_address: Address,
381 client_account_type: ClientAccountType,
382 ) -> Result<(), StoreError>;
383
384 /// Upserts the account code for a foreign account. This value will be used as a cache of known
385 /// script roots and added to the `GetForeignAccountCode` request.
386 async fn upsert_foreign_account_code(
387 &self,
388 account_id: AccountId,
389 code: AccountCode,
390 ) -> Result<(), StoreError>;
391
392 /// Retrieves the cached account code for various foreign accounts.
393 async fn get_foreign_account_code(
394 &self,
395 account_ids: Vec<AccountId>,
396 ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError>;
397
398 /// Retrieves all [`Address`] objects that correspond to the provided account ID.
399 async fn get_addresses_by_account_id(
400 &self,
401 account_id: AccountId,
402 ) -> Result<Vec<Address>, StoreError>;
403
404 /// Updates an existing [`Account`] with a new state.
405 ///
406 /// # Errors
407 ///
408 /// Returns a `StoreError::AccountDataNotFound` if there is no account for the provided ID.
409 async fn update_account(&self, new_account_state: &Account) -> Result<(), StoreError>;
410
411 /// Adds an [`Address`] to an [`Account`].
412 ///
413 /// Tag registration is the caller's responsibility — see [`Self::add_note_tag`].
414 async fn insert_address(
415 &self,
416 address: Address,
417 account_id: AccountId,
418 ) -> Result<(), StoreError>;
419
420 /// Removes an [`Address`].
421 ///
422 /// Tag removal is the caller's responsibility — see [`Self::remove_note_tag`].
423 async fn remove_address(&self, address: Address) -> Result<(), StoreError>;
424
425 // SETTINGS
426 // --------------------------------------------------------------------------------------------
427
428 /// Adds a value to the `settings` table.
429 async fn set_setting(&self, key: String, value: Vec<u8>) -> Result<(), StoreError>;
430
431 /// Retrieves a value from the `settings` table.
432 async fn get_setting(&self, key: String) -> Result<Option<Vec<u8>>, StoreError>;
433
434 /// Deletes a value from the `settings` table.
435 async fn remove_setting(&self, key: String) -> Result<(), StoreError>;
436
437 /// Returns all the keys from the `settings` table.
438 async fn list_setting_keys(&self) -> Result<Vec<String>, StoreError>;
439
440 /// Applies a batch of [`SettingMutation`]s. Use this when several `settings` entries must stay
441 /// mutually consistent (e.g. a record and its secondary index).
442 async fn apply_settings_mutations(
443 &self,
444 mutations: Vec<SettingMutation>,
445 ) -> Result<(), StoreError>;
446
447 // SYNC
448 // --------------------------------------------------------------------------------------------
449
450 /// Returns the note tag records that the client is interested in.
451 async fn get_note_tags(&self) -> Result<Vec<NoteTagRecord>, StoreError>;
452
453 /// Returns the unique note tags (without source) that the client is interested in.
454 async fn get_unique_note_tags(&self) -> Result<BTreeSet<NoteTag>, StoreError> {
455 Ok(self.get_note_tags().await?.into_iter().map(|r| r.tag).collect())
456 }
457
458 /// Adds a note tag to the list of tags that the client is interested in.
459 ///
460 /// If the tag was already being tracked, returns false since no new tags were actually added.
461 /// Otherwise true.
462 async fn add_note_tag(&self, tag: NoteTagRecord) -> Result<bool, StoreError>;
463
464 /// Removes a note tag from the list of tags that the client is interested in.
465 ///
466 /// If the tag wasn't present in the store returns false since no tag was actually removed.
467 /// Otherwise returns true.
468 async fn remove_note_tag(&self, tag: NoteTagRecord) -> Result<usize, StoreError>;
469
470 /// Returns the block number of the last state sync block.
471 async fn get_sync_height(&self) -> Result<BlockNumber, StoreError>;
472
473 /// Applies the state sync update to the store. An update involves:
474 ///
475 /// - Inserting the new block header to the store alongside new MMR peaks information.
476 /// - Updating the corresponding tracked input/output notes. Consumed notes carry consumption
477 /// metadata — `consumed_block_height`, `consumed_tx_order`, and `consumer_account_id` — in
478 /// their note state. Implementations must persist these fields so that ordered queries (see
479 /// [`Store::get_input_note_by_offset`]) work correctly.
480 /// - Removing note tags that are no longer relevant.
481 /// - Updating transactions in the store, marking as `committed` or `discarded`.
482 /// - In turn, validating private account's state transitions. If a private account's
483 /// commitment locally does not match the `StateSyncUpdate` information, the account may be
484 /// locked.
485 /// - Storing new MMR authentication nodes.
486 /// - Updating the tracked public accounts.
487 async fn apply_state_sync(&self, state_sync_update: StateSyncUpdate) -> Result<(), StoreError>;
488
489 // TRANSPORT
490 // --------------------------------------------------------------------------------------------
491
492 /// Gets the note transport cursor.
493 ///
494 /// This is used to reduce the number of fetched notes from the note transport network.
495 /// If no cursor exists, initializes it to 0.
496 async fn get_note_transport_cursor(&self) -> Result<NoteTransportCursor, StoreError> {
497 let cursor_bytes = if let Some(bytes) =
498 self.get_setting(NOTE_TRANSPORT_CURSOR_STORE_SETTING.into()).await?
499 {
500 bytes
501 } else {
502 // Lazy initialization: create cursor if not present
503 let initial = 0u64.to_be_bytes().to_vec();
504 self.set_setting(NOTE_TRANSPORT_CURSOR_STORE_SETTING.into(), initial.clone())
505 .await?;
506 initial
507 };
508 let array: [u8; 8] = cursor_bytes
509 .as_slice()
510 .try_into()
511 .map_err(|e: core::array::TryFromSliceError| StoreError::ParsingError(e.to_string()))?;
512 let cursor = u64::from_be_bytes(array);
513 Ok(cursor.into())
514 }
515
516 /// Updates the note transport cursor.
517 ///
518 /// This is used to track the last cursor position when fetching notes from the note transport
519 /// network.
520 async fn update_note_transport_cursor(
521 &self,
522 cursor: NoteTransportCursor,
523 ) -> Result<(), StoreError> {
524 let cursor_bytes = cursor.value().to_be_bytes().to_vec();
525 self.set_setting(NOTE_TRANSPORT_CURSOR_STORE_SETTING.into(), cursor_bytes)
526 .await?;
527 Ok(())
528 }
529
530 // RPC LIMITS
531 // --------------------------------------------------------------------------------------------
532
533 /// Gets persisted RPC limits. Returns `None` if not stored.
534 async fn get_rpc_limits(&self) -> Result<Option<RpcLimits>, StoreError> {
535 let Some(bytes) = self.get_setting(RPC_LIMITS_STORE_SETTING.into()).await? else {
536 return Ok(None);
537 };
538 let limits = RpcLimits::read_from_bytes(&bytes)?;
539 Ok(Some(limits))
540 }
541
542 /// Persists RPC limits to the store.
543 async fn set_rpc_limits(&self, limits: RpcLimits) -> Result<(), StoreError> {
544 self.set_setting(RPC_LIMITS_STORE_SETTING.into(), limits.to_bytes()).await
545 }
546
547 // PARTIAL MMR
548 // --------------------------------------------------------------------------------------------
549
550 /// Builds the current view of the chain's [`PartialMmr`]. Because we want to add all new
551 /// authentication nodes that could come from applying the MMR updates, we need to track all
552 /// known leaves thus far.
553 ///
554 /// The default implementation is based on [`Store::get_partial_blockchain_nodes`],
555 /// [`Store::get_current_blockchain_peaks`] and [`Store::get_block_header_by_num`]
556 async fn get_current_partial_mmr(&self) -> Result<PartialMmr, StoreError> {
557 let current_peaks = self.get_current_blockchain_peaks().await?;
558 let current_block_num = u32::try_from(current_peaks.num_leaves())
559 .map_err(|err| StoreError::ParsingError(err.to_string()))?
560 .into();
561
562 let (current_block, has_client_notes) = self
563 .get_block_header_by_num(current_block_num)
564 .await?
565 .ok_or(StoreError::BlockHeaderNotFound(current_block_num))?;
566
567 let mut current_partial_mmr = PartialMmr::from_peaks(current_peaks);
568 let has_client_notes = has_client_notes.into();
569 current_partial_mmr
570 .add(current_block.commitment(), has_client_notes)
571 .map_err(StoreError::MmrError)?;
572
573 // Build tracked_leaves from blocks that have client notes.
574 let mut tracked_leaves = self.get_tracked_block_header_numbers().await?;
575
576 // Also track the latest leaf if it is relevant (it has client notes) _and_ the forest
577 // actually has a single leaf tree bit.
578 if has_client_notes && current_partial_mmr.forest().has_single_leaf_tree() {
579 let latest_leaf = current_partial_mmr.forest().num_leaves().saturating_sub(1);
580 tracked_leaves.insert(latest_leaf);
581 }
582
583 let tracked_nodes = self
584 .get_partial_blockchain_nodes(PartialBlockchainFilter::Forest(
585 current_partial_mmr.forest(),
586 ))
587 .await?;
588
589 let current_partial_mmr =
590 PartialMmr::from_parts(current_partial_mmr.peaks(), tracked_nodes, tracked_leaves)?;
591
592 Ok(current_partial_mmr)
593 }
594
595 // ACCOUNT VAULT AND STORE
596 // --------------------------------------------------------------------------------------------
597
598 /// Retrieves the asset vault for a specific account.
599 async fn get_account_vault(&self, account_id: AccountId) -> Result<AssetVault, StoreError>;
600
601 /// Retrieves a specific asset (by vault key) from the account's vault along with its Merkle
602 /// witness.
603 ///
604 /// The default implementation of this method uses [`Store::get_account_vault`].
605 async fn get_account_asset(
606 &self,
607 account_id: AccountId,
608 vault_key: AssetVaultKey,
609 ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
610 let vault = self.get_account_vault(account_id).await?;
611 let Some(asset) = vault.assets().find(|a| a.vault_key() == vault_key) else {
612 return Ok(None);
613 };
614
615 let witness = AssetWitness::new(vault.open(vault_key).into())?;
616
617 Ok(Some((asset, witness)))
618 }
619
620 /// Retrieves the storage for a specific account.
621 ///
622 /// Can take an optional map root to retrieve only part of the storage,
623 /// If it does, it will either return an account storage with a single
624 /// slot (the one requested), or an error if not found.
625 async fn get_account_storage(
626 &self,
627 account_id: AccountId,
628 filter: AccountStorageFilter,
629 ) -> Result<AccountStorage, StoreError>;
630
631 /// Retrieves a storage slot value by name.
632 ///
633 /// For `Value` slots, returns the stored word.
634 /// For `Map` slots, returns the map root.
635 ///
636 /// The default implementation of this method uses [`Store::get_account_storage`].
637 async fn get_account_storage_item(
638 &self,
639 account_id: AccountId,
640 slot_name: StorageSlotName,
641 ) -> Result<Word, StoreError> {
642 let storage = self
643 .get_account_storage(account_id, AccountStorageFilter::SlotName(slot_name.clone()))
644 .await?;
645 storage
646 .get(&slot_name)
647 .map(StorageSlot::value)
648 .ok_or(StoreError::AccountError(AccountError::StorageSlotNameNotFound { slot_name }))
649 }
650
651 /// Retrieves a specific item from the account's storage map along with its Merkle proof.
652 ///
653 /// The default implementation of this method uses [`Store::get_account_storage`].
654 async fn get_account_map_item(
655 &self,
656 account_id: AccountId,
657 slot_name: StorageSlotName,
658 key: StorageMapKey,
659 ) -> Result<(Word, StorageMapWitness), StoreError> {
660 let storage = self
661 .get_account_storage(account_id, AccountStorageFilter::SlotName(slot_name.clone()))
662 .await?;
663 match storage.get(&slot_name).map(StorageSlot::content) {
664 Some(StorageSlotContent::Map(map)) => {
665 let value = map.get(&key);
666 let witness = map.open(&key);
667
668 Ok((value, witness))
669 },
670 Some(_) => Err(StoreError::AccountError(AccountError::StorageSlotNotMap(slot_name))),
671 None => {
672 Err(StoreError::AccountError(AccountError::StorageSlotNameNotFound { slot_name }))
673 },
674 }
675 }
676
677 // PARTIAL ACCOUNTS
678 // --------------------------------------------------------------------------------------------
679
680 /// Retrieves an [`AccountRecord`] object, this contains the account's latest partial
681 /// state along with its status. Returns `None` if the partial account is not found.
682 async fn get_minimal_partial_account(
683 &self,
684 account_id: AccountId,
685 ) -> Result<Option<AccountRecord>, StoreError>;
686}
687
688// PARTIAL BLOCKCHAIN NODE FILTER
689// ================================================================================================
690
691/// Filters for searching specific MMR nodes.
692// TODO: Should there be filters for specific blocks instead of nodes?
693pub enum PartialBlockchainFilter {
694 /// Return all nodes.
695 All,
696 /// Filter by the specified in-order indices.
697 List(Vec<InOrderIndex>),
698 /// Return nodes with in-order indices within the specified forest.
699 Forest(Forest),
700}
701
702// TRANSACTION FILTERS
703// ================================================================================================
704
705/// Filters for narrowing the set of transactions returned by the client's store.
706#[derive(Debug, Clone)]
707pub enum TransactionFilter {
708 /// Return all transactions.
709 All,
710 /// Filter by transactions that haven't yet been committed to the blockchain as per the last
711 /// sync.
712 Uncommitted,
713 /// Return a list of the transaction that matches the provided [`TransactionId`]s.
714 Ids(Vec<TransactionId>),
715 /// Return a list of the expired transactions that were executed before the provided
716 /// [`BlockNumber`]. Transactions created after the provided block number are not
717 /// considered.
718 ///
719 /// A transaction is considered expired if is uncommitted and the transaction's block number
720 /// is less than the provided block number.
721 ExpiredBefore(BlockNumber),
722}
723
724// TRANSACTIONS FILTER HELPERS
725// ================================================================================================
726
727impl TransactionFilter {
728 /// Returns a [String] containing the query for this Filter.
729 pub fn to_query(&self) -> String {
730 const QUERY: &str = "SELECT tx.id, script.script, tx.details, tx.status \
731 FROM transactions AS tx LEFT JOIN transaction_scripts AS script ON tx.script_root = script.script_root";
732 match self {
733 TransactionFilter::All => QUERY.to_string(),
734 TransactionFilter::Uncommitted => format!(
735 "{QUERY} WHERE tx.status_variant = {}",
736 TransactionStatusVariant::Pending as u8,
737 ),
738 TransactionFilter::Ids(_) => {
739 // Use SQLite's array parameter binding
740 format!("{QUERY} WHERE tx.id IN rarray(?)")
741 },
742 TransactionFilter::ExpiredBefore(block_num) => {
743 format!(
744 "{QUERY} WHERE tx.block_num < {} AND tx.status_variant != {} AND tx.status_variant != {}",
745 block_num.as_u32(),
746 TransactionStatusVariant::Discarded as u8,
747 TransactionStatusVariant::Committed as u8
748 )
749 },
750 }
751 }
752}
753
754// NOTE FILTER
755// ================================================================================================
756
757/// Filters for narrowing the set of notes returned by the client's store.
758#[derive(Debug, Clone)]
759pub enum NoteFilter {
760 /// Return a list of all notes ([`InputNoteRecord`] or [`OutputNoteRecord`]).
761 All,
762 /// Return a list of committed notes ([`InputNoteRecord`] or [`OutputNoteRecord`]). These
763 /// represent notes that the blockchain has included in a block.
764 Committed,
765 /// Filter by consumed notes ([`InputNoteRecord`] or [`OutputNoteRecord`]). notes that have
766 /// been used as inputs in transactions.
767 Consumed,
768 /// Return a list of expected notes ([`InputNoteRecord`] or [`OutputNoteRecord`]). These
769 /// represent notes for which the store doesn't have anchor data.
770 Expected,
771 /// Return a list containing any notes that match with the provided [`NoteId`] vector.
772 List(Vec<NoteId>),
773 /// Return a list containing any notes whose details commitment matches one of the provided
774 /// [`NoteDetailsCommitment`] vector. Unlike [`NoteFilter::List`], this matches the
775 /// metadata-independent details commitment, so it also resolves metadata-less notes (which
776 /// have a NULL `note_id`).
777 DetailsCommitments(Vec<NoteDetailsCommitment>),
778 /// Return a list containing any notes that match the provided [`Nullifier`] vector.
779 Nullifiers(Vec<Nullifier>),
780 /// Return a list of notes that are currently being processed. This filter doesn't apply to
781 /// output notes.
782 Processing,
783 /// Return a list containing the note that matches with the provided [`NoteId`]. The query will
784 /// return an error if the note isn't found.
785 Unique(NoteId),
786 /// Return a list containing notes that haven't been nullified yet, this includes expected,
787 /// committed, processing and unverified notes.
788 Unspent,
789 /// Return a list containing notes with unverified inclusion proofs. This filter doesn't apply
790 /// to output notes.
791 Unverified,
792}
793
794// BLOCK RELEVANCE
795// ================================================================================================
796
797/// Expresses metadata about the block header.
798#[derive(Debug, Clone)]
799pub enum BlockRelevance {
800 /// The block header includes notes that the client may consume.
801 HasNotes,
802 /// The block header does not contain notes relevant to the client.
803 Irrelevant,
804}
805
806impl From<BlockRelevance> for bool {
807 fn from(val: BlockRelevance) -> Self {
808 match val {
809 BlockRelevance::HasNotes => true,
810 BlockRelevance::Irrelevant => false,
811 }
812 }
813}
814
815impl From<bool> for BlockRelevance {
816 fn from(has_notes: bool) -> Self {
817 if has_notes {
818 BlockRelevance::HasNotes
819 } else {
820 BlockRelevance::Irrelevant
821 }
822 }
823}
824
825// STORAGE FILTER
826// ================================================================================================
827
828/// Filters for narrowing the storage slots returned by the client's store.
829#[derive(Debug, Clone)]
830pub enum AccountStorageFilter {
831 /// Return an [`AccountStorage`] with all available slots.
832 All,
833 /// Return an [`AccountStorage`] with a single slot that matches the provided [`Word`] map root.
834 Root(Word),
835 /// Return an [`AccountStorage`] with a single slot that matches the provided slot name.
836 SlotName(StorageSlotName),
837 /// Return an [`AccountStorage`] containing only the slots whose names are in the provided
838 /// list. Useful to avoid loading the full storage when only a known subset of slots is needed
839 /// (e.g. when applying a delta to a large account).
840 SlotNames(Vec<StorageSlotName>),
841}