1use std::collections::{BTreeMap, BTreeSet, HashSet};
2use std::mem::size_of;
3use std::num::NonZeroUsize;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use anyhow::Context;
8use miden_node_db::sqlite::{DbReader, DbWriter, WriteTx};
9use miden_node_proto::domain::account::AccountInfo;
10use miden_node_tracing::{info, miden_instrument, warn};
11use miden_node_utils::limiter::{
12 MAX_RESPONSE_PAYLOAD_BYTES,
13 QueryParamLimiter,
14 QueryParamNoteCommitmentLimit,
15};
16use miden_protocol::Word;
17use miden_protocol::account::{AccountHeader, AccountId, AccountStorageHeader, StorageMapKey};
18use miden_protocol::asset::{Asset, AssetId};
19use miden_protocol::block::{
20 BlockAccountUpdate,
21 BlockHeader,
22 BlockNoteIndex,
23 BlockNumber,
24 BlockSignatures,
25 SignedBlock,
26};
27use miden_protocol::crypto::merkle::SparseMerklePath;
28use miden_protocol::note::{
29 NoteAttachments,
30 NoteDetails,
31 NoteId,
32 NoteInclusionProof,
33 NoteMetadata,
34 NoteScript,
35 Nullifier,
36};
37use miden_protocol::protocol_config::ProtocolConfig;
38use miden_protocol::transaction::TransactionHeader;
39
40use crate::db::migrations::{migrate_database, verify_latest_schema};
41pub use crate::db::queries::{
42 AccountCommitmentsPage,
43 HISTORICAL_BLOCK_RETENTION,
44 NullifiersPage,
45 PrecomputedPublicAccountState,
46 PrecomputedPublicAccountStates,
47 PublicAccountIdsPage,
48 PublicAccountStateRootsPage,
49 StorageMapValuesPage,
50};
51use crate::errors::{DatabaseError, NoteSyncError};
52use crate::genesis::GenesisBlock;
53use crate::state::{ScopedBlockNum, ScopedBlockRange};
54use crate::{COMPONENT, LOG_TARGET};
55
56const STORAGE_MAP_VALUE_PER_ROW_BYTES: usize =
57 2 * size_of::<Word>() + size_of::<u32>() + size_of::<u8>();
58
59fn default_storage_map_entries_limit() -> usize {
60 MAX_RESPONSE_PAYLOAD_BYTES / STORAGE_MAP_VALUE_PER_ROW_BYTES
61}
62
63mod migrations;
64#[cfg(test)]
65pub(crate) use migrations::bootstrap_database;
66
67#[cfg(test)]
68mod tests;
69
70#[cfg(test)]
71mod test_db;
72#[cfg(test)]
73pub(crate) use test_db::TestDb;
74
75pub(crate) mod queries;
77
78mod utils;
79
80pub type Result<T, E = DatabaseError> = std::result::Result<T, E>;
81
82#[derive(Copy, Clone, Debug, PartialEq, Eq)]
84pub struct DatabaseOptions {
85 pub connection_pool_size: NonZeroUsize,
87}
88
89impl Default for DatabaseOptions {
90 fn default() -> Self {
91 Self {
92 connection_pool_size: miden_node_db::default_connection_pool_size(),
93 }
94 }
95}
96
97pub struct Db {
102 writer: DbWriter,
103 reader: DbReader,
104}
105
106fn insert_genesis(tx: &WriteTx<'_>, genesis: GenesisBlock) -> Result<()> {
108 let (genesis_block, protocol_config) = genesis.into_parts();
109 let new_account_ids = genesis_block
111 .body()
112 .updated_accounts()
113 .iter()
114 .map(BlockAccountUpdate::account_id)
115 .collect();
116 queries::insert_protocol_config(tx, &protocol_config, BlockNumber::GENESIS)?;
117 queries::apply_block(
118 tx,
119 &genesis_block,
120 &[],
121 &PrecomputedPublicAccountStates::new(),
122 &new_account_ids,
123 )?;
124 Ok(())
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132#[repr(transparent)]
133pub struct BlockHeaderCommitment(pub(crate) Word);
134
135impl BlockHeaderCommitment {
136 pub fn new(header: &BlockHeader) -> Self {
137 Self(header.commitment())
138 }
139
140 pub fn word(self) -> Word {
141 self.0
142 }
143}
144
145#[derive(Debug, Clone)]
149pub struct AccountVaultValue {
150 pub block_num: BlockNumber,
151 pub vault_key: AssetId,
152 pub asset: Option<Asset>,
154}
155
156#[derive(Debug, PartialEq)]
157pub struct NullifierInfo {
158 pub nullifier: Nullifier,
159 pub block_num: BlockNumber,
160}
161
162impl PartialEq<(Nullifier, BlockNumber)> for NullifierInfo {
163 fn eq(&self, (nullifier, block_num): &(Nullifier, BlockNumber)) -> bool {
164 &self.nullifier == nullifier && &self.block_num == block_num
165 }
166}
167
168#[derive(Debug, PartialEq)]
169pub struct TransactionRecord {
170 pub block_num: BlockNumber,
171 pub header: TransactionHeader,
172 pub output_note_proofs: Vec<NoteSyncRecord>,
175 pub consumed_note_refs: Vec<(Nullifier, NoteId)>,
178}
179
180#[derive(Debug, Clone, PartialEq)]
181pub struct NoteRecord {
182 pub block_num: BlockNumber,
183 pub note_index: BlockNoteIndex,
184 pub note_id: Word,
185 pub metadata: NoteMetadata,
186 pub details: Option<NoteDetails>,
187 pub attachments: NoteAttachments,
188 pub inclusion_path: SparseMerklePath,
189}
190
191#[derive(Debug, PartialEq)]
192pub struct NoteSyncUpdate {
193 pub notes: Vec<NoteSyncRecord>,
194 pub block_header: BlockHeader,
195}
196
197#[derive(Debug, Clone, PartialEq)]
198pub struct NoteSyncRecord {
199 pub block_num: BlockNumber,
200 pub note_index: BlockNoteIndex,
201 pub note_id: NoteId,
202 pub metadata: NoteMetadata,
203 pub attachments: NoteAttachments,
204 pub inclusion_path: SparseMerklePath,
205}
206
207impl From<NoteRecord> for NoteSyncRecord {
208 fn from(note: NoteRecord) -> Self {
209 Self {
210 block_num: note.block_num,
211 note_index: note.note_index,
212 note_id: NoteId::from_raw(note.note_id),
213 metadata: note.metadata,
214 attachments: note.attachments,
215 inclusion_path: note.inclusion_path,
216 }
217 }
218}
219
220impl Db {
221 #[miden_instrument(
223 target = COMPONENT,
224 name = "store.database.bootstrap",
225 fields(path = database_filepath),
226 err,
227 )]
228 pub async fn bootstrap(
229 database_filepath: PathBuf,
230 genesis: GenesisBlock,
231 ) -> anyhow::Result<()> {
232 migrations::bootstrap_database(&database_filepath)
233 .context("failed to bootstrap database schema")?;
234
235 let (writer, _reader) = miden_node_db::sqlite::open(&database_filepath)
236 .context("failed to open a database connection")?;
237
238 writer
240 .write("insert genesis block", move |tx| insert_genesis(tx, genesis))
241 .await
242 .context("failed to insert genesis block")?;
243 Ok(())
244 }
245
246 #[miden_instrument(
248 target = COMPONENT,
249 )]
250 pub async fn load(database_filepath: PathBuf) -> Result<Self, DatabaseError> {
251 Self::load_with_pool_size(database_filepath, miden_node_db::default_connection_pool_size())
252 .await
253 }
254
255 #[miden_instrument(
258 target = COMPONENT,
259 )]
260 pub async fn load_with_pool_size(
261 database_filepath: PathBuf,
262 connection_pool_size: NonZeroUsize,
263 ) -> Result<Self, DatabaseError> {
264 verify_latest_schema(&database_filepath)?;
265
266 let (writer, reader) =
267 miden_node_db::sqlite::open_with_pool_size(&database_filepath, connection_pool_size)?;
268 info!(
269 target: LOG_TARGET,
270 "Connected to the database",
271 path = database_filepath,
272 db.sqlite.connection_pool_size = connection_pool_size.get()
273 );
274
275 Ok(Self { writer, reader })
276 }
277
278 #[cfg(test)]
280 pub(crate) fn writer(&self) -> &DbWriter {
281 &self.writer
282 }
283
284 #[miden_instrument(
286 level = "debug",
287 target = COMPONENT,
288 err,
289 )]
290 pub async fn select_protocol_config_by_commitment(
291 &self,
292 commitment: Word,
293 ) -> Result<Option<ProtocolConfig>> {
294 self.reader
295 .read("protocol config by commitment", move |tx| {
296 queries::select_protocol_config_by_commitment(tx, commitment)
297 })
298 .await
299 }
300
301 pub async fn select_protocol_config_commitment_at(
303 &self,
304 block_number: ScopedBlockNum,
305 ) -> Result<Option<Word>> {
306 self.reader
307 .read("protocol config commitment at block", move |tx| {
308 queries::select_protocol_config_commitment_at(tx, *block_number)
309 })
310 .await
311 }
312
313 #[miden_instrument(
315 target = COMPONENT,
316 )]
317 pub fn migrate(database_filepath: impl AsRef<Path>) -> Result<(), DatabaseError> {
318 migrate_database(database_filepath.as_ref())?;
319 Ok(())
320 }
321
322 #[miden_instrument(
324 level = "debug",
325 target = COMPONENT,
326 err,
327 )]
328 pub async fn select_nullifiers_paged(
329 &self,
330 page_size: std::num::NonZeroUsize,
331 after_nullifier: Option<Nullifier>,
332 ) -> Result<NullifiersPage> {
333 self.reader
334 .read("read nullifiers paged", move |tx| {
335 queries::select_nullifiers_paged(tx, page_size, after_nullifier)
336 })
337 .await
338 }
339
340 #[miden_instrument(
342 level = "debug",
343 target = COMPONENT,
344 fields(
345 prefix_len,
346 prefix.count = nullifier_prefixes.len(),
347 ),
348 err,
349 )]
350 pub async fn select_nullifiers_by_prefix(
351 &self,
352 prefix_len: u32,
353 nullifier_prefixes: Vec<u32>,
354 block_range: ScopedBlockRange,
355 ) -> Result<(Vec<NullifierInfo>, BlockNumber)> {
356 let block_range = block_range.into_inner();
357 assert_eq!(prefix_len, 16, "Only 16-bit prefixes are supported");
358
359 self.reader
360 .read("nullifieres by prefix", move |tx| {
361 let nullifier_prefixes =
362 nullifier_prefixes.into_iter().map(|prefix| prefix as u16).collect::<Vec<_>>();
363 queries::select_nullifiers_by_prefix(
364 tx,
365 prefix_len as u8,
366 &nullifier_prefixes[..],
367 block_range,
368 )
369 })
370 .await
371 }
372
373 #[miden_instrument(
377 level = "debug",
378 target = COMPONENT,
379 err,
380 )]
381 pub async fn select_block_header_by_block_num(
382 &self,
383 maybe_block_number: Option<ScopedBlockNum>,
384 ) -> Result<Option<BlockHeader>> {
385 self.reader
386 .read("block headers by block number", move |tx| {
387 queries::select_block_header_by_block_num(
388 tx,
389 maybe_block_number.map(|block_number| *block_number),
390 )
391 })
392 .await
393 }
394
395 pub(crate) async fn select_genesis_block_header(&self) -> Result<Option<BlockHeader>> {
397 self.reader
398 .read("genesis block header", |tx| {
399 queries::select_block_header_by_block_num(tx, Some(BlockNumber::GENESIS))
400 })
401 .await
402 }
403
404 #[miden_instrument(
407 level = "debug",
408 target = COMPONENT,
409 err,
410 )]
411 pub async fn select_block_header_and_signatures_by_block_num(
412 &self,
413 block_number: ScopedBlockNum,
414 ) -> Result<Option<(BlockHeader, BlockSignatures)>> {
415 self.reader
416 .read("block headers and signatures by block number", move |tx| {
417 queries::select_block_header_and_signatures_by_block_num(tx, *block_number)
418 })
419 .await
420 }
421
422 #[miden_instrument(
424 level = "debug",
425 target = COMPONENT,
426 err,
427 )]
428 pub async fn select_block_headers(
429 &self,
430 blocks: impl Iterator<Item = ScopedBlockNum> + Send + 'static,
431 ) -> Result<Vec<BlockHeader>> {
432 self.reader
433 .read("block headers from given block numbers", move |tx| {
434 queries::select_block_headers(tx, blocks.map(|block| *block))
435 })
436 .await
437 }
438
439 #[miden_instrument(
441 level = "debug",
442 target = COMPONENT,
443 err,
444 )]
445 pub async fn select_all_block_header_commitments(&self) -> Result<Vec<BlockHeaderCommitment>> {
446 self.reader
447 .read("all block headers", queries::select_all_block_header_commitments)
448 .await
449 }
450
451 #[miden_instrument(
453 level = "debug",
454 target = COMPONENT,
455 err,
456 )]
457 pub async fn select_account_commitments_paged(
458 &self,
459 page_size: std::num::NonZeroUsize,
460 after_account_id: Option<AccountId>,
461 ) -> Result<AccountCommitmentsPage> {
462 self.reader
463 .read("read account commitments paged", move |tx| {
464 queries::select_account_commitments_paged(tx, page_size, after_account_id)
465 })
466 .await
467 }
468
469 #[miden_instrument(
471 level = "debug",
472 target = COMPONENT,
473 err,
474 )]
475 pub async fn select_public_account_ids_paged(
476 &self,
477 page_size: std::num::NonZeroUsize,
478 after_account_id: Option<AccountId>,
479 ) -> Result<PublicAccountIdsPage> {
480 self.reader
481 .read("read public account IDs paged", move |tx| {
482 queries::select_public_account_ids_paged(tx, page_size, after_account_id)
483 })
484 .await
485 }
486
487 #[miden_instrument(
489 level = "debug",
490 target = COMPONENT,
491 err,
492 )]
493 pub async fn select_public_account_state_roots_paged(
494 &self,
495 page_size: std::num::NonZeroUsize,
496 after_account_id: Option<AccountId>,
497 ) -> Result<PublicAccountStateRootsPage> {
498 self.reader
499 .read("read public account state roots paged", move |tx| {
500 queries::select_public_account_state_roots_paged(tx, page_size, after_account_id)
501 })
502 .await
503 }
504
505 #[miden_instrument(
507 level = "debug",
508 target = COMPONENT,
509 err,
510 )]
511 pub async fn select_account(&self, id: AccountId) -> Result<AccountInfo> {
512 self.reader
513 .read("Get account details", move |tx| queries::select_account(tx, id))
514 .await
515 }
516
517 #[miden_instrument(
519 level = "debug",
520 target = COMPONENT,
521 err,
522 )]
523 pub async fn filter_network_accounts(
524 &self,
525 account_ids: Vec<AccountId>,
526 ) -> Result<HashSet<AccountId>> {
527 self.reader
528 .read("Filter network accounts", move |tx| {
529 queries::filter_network_accounts(tx, &account_ids)
530 })
531 .await
532 }
533
534 #[miden_instrument(
538 target = COMPONENT,
539 )]
540 pub async fn select_account_code_by_commitment(
541 &self,
542 code_commitment: Word,
543 ) -> Result<Option<miden_protocol::account::AccountCode>> {
544 self.reader
545 .read("Get account code by commitment", move |tx| {
546 queries::select_account_code_by_commitment(tx, code_commitment)?
547 .map(|bytes| {
548 miden_node_persistence::decode::<miden_protocol::account::AccountCode>(
549 &bytes,
550 )
551 })
552 .transpose()
553 .map_err(DatabaseError::from)
554 })
555 .await
556 }
557
558 #[miden_instrument(
563 target = COMPONENT,
564 )]
565 pub async fn select_account_header_with_storage_header_at_block(
566 &self,
567 account_id: AccountId,
568 block_num: ScopedBlockNum,
569 ) -> Result<Option<(AccountHeader, AccountStorageHeader)>> {
570 self.reader
571 .read("Get account header with storage header at block", move |tx| {
572 queries::select_account_header_with_storage_header_at_block(
573 tx, account_id, *block_num,
574 )
575 })
576 .await
577 }
578
579 #[miden_instrument(
580 level = "debug",
581 target = COMPONENT,
582 err,
583 )]
584 pub async fn get_note_sync_multi(
585 &self,
586 block_range: ScopedBlockRange,
587 note_tags: Arc<[u32]>,
588 ) -> Result<Vec<NoteSyncUpdate>, NoteSyncError> {
589 let block_range = block_range.into_inner();
590 self.reader
591 .read("notes sync task", move |tx| {
592 queries::get_note_sync_multi(
593 tx,
594 ¬e_tags,
595 block_range,
596 MAX_RESPONSE_PAYLOAD_BYTES,
597 )
598 })
599 .await
600 }
601
602 #[miden_instrument(
605 level = "debug",
606 target = COMPONENT,
607 err,
608 )]
609 pub async fn select_notes_by_id(&self, note_ids: Vec<NoteId>) -> Result<Vec<NoteRecord>> {
610 self.reader
611 .read("note by id", move |tx| queries::select_notes_by_id(tx, note_ids.as_slice()))
612 .await
613 }
614
615 #[miden_instrument(
617 level = "debug",
618 target = COMPONENT,
619 err,
620 )]
621 pub async fn select_existing_note_ids(
622 &self,
623 note_ids: Vec<NoteId>,
624 up_to_block: ScopedBlockNum,
625 ) -> Result<HashSet<NoteId>> {
626 self.reader
627 .read("existing note IDs", move |tx| {
628 queries::select_existing_note_ids(tx, note_ids.as_slice(), *up_to_block)
629 })
630 .await
631 }
632
633 #[miden_instrument(
636 level = "debug",
637 target = COMPONENT,
638 err,
639 )]
640 pub async fn select_note_inclusion_proofs(
641 &self,
642 note_commitments: BTreeSet<Word>,
643 up_to_block: ScopedBlockNum,
644 ) -> Result<BTreeMap<NoteId, NoteInclusionProof>> {
645 self.reader
646 .read("block note inclusion proofs by commitment", move |tx| {
647 queries::select_note_inclusion_proofs(tx, ¬e_commitments, *up_to_block)
648 })
649 .await
650 }
651
652 #[expect(
668 clippy::too_many_arguments,
669 reason = "the arguments are the block and the state that the writer precomputed for it"
670 )]
671 #[miden_instrument(
672 target = COMPONENT,
673 err,
674 )]
675 pub(crate) async fn apply_block(
676 &self,
677 signed_block: SignedBlock,
678 activated_protocol_config: Option<ProtocolConfig>,
679 notes: Vec<(NoteRecord, Option<Nullifier>)>,
680 precomputed_public_states: PrecomputedPublicAccountStates,
681 new_account_ids: BTreeSet<AccountId>,
682 unresolved_note_nullifiers: Vec<Nullifier>,
683 prune_tip: BlockNumber,
684 ) -> Result<BTreeMap<Nullifier, NoteId>> {
685 self.writer
686 .write::<_, DatabaseError, _>("apply block", move |tx| {
687 if let Some(protocol_config) = activated_protocol_config.as_ref() {
688 queries::insert_protocol_config(
689 tx,
690 protocol_config,
691 signed_block.header().block_num(),
692 )?;
693 }
694 queries::apply_block(
695 tx,
696 &signed_block,
697 ¬es,
698 &precomputed_public_states,
699 &new_account_ids,
700 )?;
701 queries::prune_history(tx, prune_tip)?;
702 Ok(())
703 })
704 .await?;
705
706 Ok(self.resolve_consumed_note_ids(unresolved_note_nullifiers).await)
707 }
708
709 async fn resolve_consumed_note_ids(
714 &self,
715 nullifiers: Vec<Nullifier>,
716 ) -> BTreeMap<Nullifier, NoteId> {
717 let mut resolved_note_ids = BTreeMap::new();
718 for chunk in nullifiers.chunks(QueryParamNoteCommitmentLimit::LIMIT) {
719 let chunk = chunk.to_vec();
720 let count = chunk.len();
721 let result = self
722 .reader
723 .read("resolve consumed note ids", move |tx| {
724 queries::select_note_ids_by_nullifier(tx, &chunk)
725 })
726 .await;
727
728 match result {
729 Ok(note_ids) => resolved_note_ids.extend(note_ids),
730 Err(err) => {
731 warn!(
732 &err,
733 target: COMPONENT,
734 "Failed to resolve consumed note IDs for lifecycle events",
735 note.nullifier.count = count
736 );
737 break;
738 },
739 }
740 }
741
742 resolved_note_ids
743 }
744
745 pub(crate) async fn select_storage_map_sync_values(
750 &self,
751 account_id: AccountId,
752 block_range: ScopedBlockRange,
753 entries_limit: Option<usize>,
754 ) -> Result<StorageMapValuesPage> {
755 let block_range = block_range.into_inner();
756 let entries_limit = entries_limit.unwrap_or_else(default_storage_map_entries_limit);
757
758 self.reader
759 .read("select storage map sync values", move |tx| {
760 queries::select_account_storage_map_values_paged(
761 tx,
762 account_id,
763 block_range,
764 entries_limit,
765 )
766 })
767 .await
768 }
769
770 #[miden_instrument(
779 target = COMPONENT,
780 )]
781 pub(crate) async fn reconstruct_storage_map_from_db(
782 &self,
783 account_id: AccountId,
784 slot_name: miden_protocol::account::StorageSlotName,
785 block_num: ScopedBlockNum,
786 entries_limit: Option<usize>,
787 ) -> Result<miden_node_proto::domain::account::AccountStorageMapDetails> {
788 use miden_node_proto::domain::account::{AccountStorageMapDetails, StorageMapEntries};
789 use miden_protocol::EMPTY_WORD;
790
791 let mut values = Vec::new();
794 let mut block_range_start = BlockNumber::GENESIS;
795 let entries_limit = entries_limit.unwrap_or_else(default_storage_map_entries_limit);
796
797 let mut page = self
798 .select_storage_map_sync_values(
799 account_id,
800 block_num.range_from(block_range_start),
801 Some(entries_limit),
802 )
803 .await?;
804
805 values.extend(page.values);
806 let mut last_block_included = page.last_block_included;
807
808 if values.is_empty() && last_block_included == block_range_start {
811 return Ok(AccountStorageMapDetails::limit_exceeded(slot_name));
812 }
813
814 loop {
815 if page.last_block_included == *block_num
816 || page.last_block_included < block_range_start
817 {
818 break;
819 }
820
821 block_range_start = page.last_block_included.child();
822 page = self
823 .select_storage_map_sync_values(
824 account_id,
825 block_num.range_from(block_range_start),
826 Some(entries_limit),
827 )
828 .await?;
829
830 if page.last_block_included <= last_block_included {
831 return Ok(AccountStorageMapDetails::limit_exceeded(slot_name));
832 }
833
834 last_block_included = page.last_block_included;
835 values.extend(page.values);
836 }
837
838 if page.last_block_included != *block_num {
839 return Ok(AccountStorageMapDetails::limit_exceeded(slot_name));
840 }
841
842 let mut latest_values = BTreeMap::<StorageMapKey, Word>::new();
844 for value in values {
845 if value.slot_name == slot_name {
846 let raw_key = value.key;
847 latest_values.insert(raw_key, value.value);
848 }
849 }
850
851 latest_values.retain(|_, v| *v != EMPTY_WORD);
853
854 if latest_values.len() > AccountStorageMapDetails::MAX_RETURN_ENTRIES {
855 return Ok(AccountStorageMapDetails::limit_exceeded(slot_name));
856 }
857
858 let entries = latest_values.into_iter().collect::<Vec<_>>();
859 Ok(AccountStorageMapDetails {
860 slot_name,
861 entries: StorageMapEntries::AllEntries(entries),
862 })
863 }
864
865 #[miden_instrument(
870 target = COMPONENT,
871 )]
872 pub async fn select_vault_at_block(
873 &self,
874 account_id: AccountId,
875 block_num: ScopedBlockNum,
876 ) -> Result<Vec<Asset>, DatabaseError> {
877 self.reader
878 .read("select vault at block", move |tx| {
879 queries::select_vault_at_block(tx, account_id, *block_num)
880 })
881 .await
882 }
883
884 pub async fn get_account_vault_sync(
885 &self,
886 account_id: AccountId,
887 block_range: ScopedBlockRange,
888 ) -> Result<(BlockNumber, Vec<AccountVaultValue>)> {
889 let block_range = block_range.into_inner();
890 self.reader
891 .read("account vault sync", move |tx| {
892 queries::select_account_vault_assets(tx, account_id, block_range)
893 })
894 .await
895 }
896
897 pub async fn select_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>> {
899 self.reader
900 .read("note script by root", move |tx| queries::select_note_script_by_root(tx, root))
901 .await
902 }
903
904 pub async fn select_transactions_records(
911 &self,
912 account_ids: Vec<AccountId>,
913 block_range: ScopedBlockRange,
914 ) -> Result<(BlockNumber, Vec<TransactionRecord>)> {
915 let block_range = block_range.into_inner();
916 self.reader
917 .read("full transactions records", move |tx| {
918 queries::select_transactions_records(tx, &account_ids, block_range)
919 })
920 .await
921 }
922}