1use std::boxed::Box;
8use std::collections::{BTreeMap, BTreeSet};
9use std::path::{Path, PathBuf};
10use std::string::{String, ToString};
11use std::time::Duration;
12use std::vec::Vec;
13
14use db_management::migration::SqliteMigrator;
15use db_management::pool_manager::{Pool, SqlitePoolManager};
16use deadpool::Runtime;
17use miden_client::Word;
18use miden_client::account::{
19 Account,
20 AccountCode,
21 AccountHeader,
22 AccountId,
23 AccountStorage,
24 Address,
25 StorageMapKey,
26 StorageSlotName,
27};
28use miden_client::asset::{Asset, AssetVault, AssetWitness};
29use miden_client::block::BlockHeader;
30use miden_client::crypto::{InOrderIndex, MmrPeaks};
31use miden_client::note::{BlockNumber, NoteScript, NoteTag, Nullifier};
32use miden_client::store::{
33 AccountRecord,
34 AccountStatus,
35 AccountStorageFilter,
36 BlockRelevance,
37 ClientAccountType,
38 InputNoteCursor,
39 InputNoteRecord,
40 NoteFilter,
41 OutputNoteRecord,
42 PartialBlockchainFilter,
43 SettingMutation,
44 SettingScope,
45 Store,
46 StoreError,
47 TransactionFilter,
48};
49use miden_client::sync::{NoteTagRecord, StateSyncUpdate};
50use miden_client::transaction::{TransactionRecord, TransactionStoreUpdate};
51use miden_protocol::Felt;
52use miden_protocol::account::StorageMapWitness;
53use miden_protocol::asset::AssetId;
54use rusqlite::Connection;
55use rusqlite::types::Value;
56use sql_error::SqlResultExt;
57
58use crate::account::helpers::query_vault_assets;
59
60mod account;
61mod builder;
62mod chain_data;
63mod db_management;
64mod forest;
65mod note;
66mod settings;
67mod sql_error;
68mod sync;
69mod transaction;
70
71pub use builder::ClientBuilderSqliteExt;
72
73pub struct SqliteStore {
81 pub(crate) pool: Pool,
82 database_filepath: PathBuf,
83}
84
85impl SqliteStore {
86 pub async fn new(database_filepath: PathBuf) -> Result<Self, StoreError> {
91 if database_filepath.to_str().is_none() {
92 return Err(database_error(format!(
93 "database path is not valid UTF-8: {}",
94 database_filepath.display()
95 )));
96 }
97
98 let sqlite_pool_manager = SqlitePoolManager::new(database_filepath.clone());
99 let pool = Pool::builder(sqlite_pool_manager)
100 .wait_timeout(Some(Duration::from_secs(30)))
101 .runtime(Runtime::Tokio1)
102 .build()
103 .map_err(database_error)?;
104
105 Self::migrate(&pool, SqliteMigrator::client()).await?;
106
107 Ok(SqliteStore { pool, database_filepath })
110 }
111
112 pub fn database_filepath(&self) -> &Path {
114 &self.database_filepath
115 }
116
117 async fn migrate(pool: &Pool, migration: &'static SqliteMigrator) -> Result<(), StoreError> {
122 let conn = pool.get().await.map_err(database_error)?;
123
124 conn.interact(move |conn| migration.apply(conn))
125 .await
126 .map_err(database_error)?
127 .map_err(database_error)
128 }
129
130 async fn interact_with_connection<F, R>(&self, f: F) -> Result<R, StoreError>
137 where
138 F: FnOnce(&mut Connection) -> Result<R, StoreError> + Send + 'static,
139 R: Send + 'static,
140 {
141 self.pool
142 .get()
143 .await
144 .map_err(database_error)?
145 .interact(f)
146 .await
147 .map_err(database_error)?
148 }
149}
150
151#[async_trait::async_trait]
156impl Store for SqliteStore {
157 fn identifier(&self) -> &str {
158 self.database_filepath
159 .to_str()
160 .expect("rejected by SqliteStore::new when not UTF-8")
161 }
162
163 fn get_current_timestamp(&self) -> Option<u64> {
164 Some(current_timestamp_u64())
165 }
166
167 async fn get_note_tags(&self) -> Result<Vec<NoteTagRecord>, StoreError> {
168 self.interact_with_connection(SqliteStore::get_note_tags).await
169 }
170
171 async fn get_unique_note_tags(&self) -> Result<BTreeSet<NoteTag>, StoreError> {
172 self.interact_with_connection(SqliteStore::get_unique_note_tags).await
173 }
174
175 async fn add_note_tag(&self, tag: NoteTagRecord) -> Result<bool, StoreError> {
176 self.interact_with_connection(move |conn| SqliteStore::add_note_tag(conn, tag))
177 .await
178 }
179
180 async fn remove_note_tag(&self, tag: NoteTagRecord) -> Result<usize, StoreError> {
181 self.interact_with_connection(move |conn| SqliteStore::remove_note_tag(conn, tag))
182 .await
183 }
184
185 async fn get_sync_height(&self) -> Result<BlockNumber, StoreError> {
186 self.interact_with_connection(SqliteStore::get_sync_height).await
187 }
188
189 async fn apply_state_sync(&self, state_sync_update: StateSyncUpdate) -> Result<(), StoreError> {
190 self.interact_with_connection(move |conn| {
191 SqliteStore::apply_state_sync(conn, state_sync_update)
192 })
193 .await
194 }
195
196 async fn get_transactions(
197 &self,
198 transaction_filter: TransactionFilter,
199 ) -> Result<Vec<TransactionRecord>, StoreError> {
200 self.interact_with_connection(move |conn| {
201 SqliteStore::get_transactions(conn, &transaction_filter)
202 })
203 .await
204 }
205
206 async fn apply_transaction(&self, tx_update: TransactionStoreUpdate) -> Result<(), StoreError> {
207 self.interact_with_connection(move |conn| SqliteStore::apply_transaction(conn, &tx_update))
208 .await
209 }
210
211 async fn apply_transaction_batch(
212 &self,
213 tx_updates: Vec<TransactionStoreUpdate>,
214 ) -> Result<(), StoreError> {
215 self.interact_with_connection(move |conn| {
216 SqliteStore::apply_transaction_batch(conn, &tx_updates)
217 })
218 .await
219 }
220
221 async fn get_input_notes(
222 &self,
223 filter: NoteFilter,
224 ) -> Result<Vec<InputNoteRecord>, StoreError> {
225 self.interact_with_connection(move |conn| SqliteStore::get_input_notes(conn, &filter))
226 .await
227 }
228
229 async fn get_output_notes(
230 &self,
231 note_filter: NoteFilter,
232 ) -> Result<Vec<OutputNoteRecord>, StoreError> {
233 self.interact_with_connection(move |conn| SqliteStore::get_output_notes(conn, ¬e_filter))
234 .await
235 }
236
237 async fn get_input_note_after(
238 &self,
239 filter: NoteFilter,
240 consumer: AccountId,
241 block_start: Option<BlockNumber>,
242 block_end: Option<BlockNumber>,
243 cursor: Option<InputNoteCursor>,
244 ) -> Result<Option<InputNoteRecord>, StoreError> {
245 self.interact_with_connection(move |conn| {
246 SqliteStore::get_input_note_after(
247 conn,
248 &filter,
249 consumer,
250 block_start,
251 block_end,
252 cursor,
253 )
254 })
255 .await
256 }
257
258 async fn upsert_input_notes(&self, notes: &[InputNoteRecord]) -> Result<(), StoreError> {
259 let notes = notes.to_vec();
260 self.interact_with_connection(move |conn| SqliteStore::upsert_input_notes(conn, ¬es))
261 .await
262 }
263
264 async fn get_note_script(&self, script_root: Word) -> Result<NoteScript, StoreError> {
265 self.interact_with_connection(move |conn| SqliteStore::get_note_script(conn, script_root))
266 .await
267 }
268
269 async fn upsert_note_scripts(&self, note_scripts: &[NoteScript]) -> Result<(), StoreError> {
270 let note_scripts = note_scripts.to_vec();
271 self.interact_with_connection(move |conn| {
272 SqliteStore::upsert_note_scripts(conn, ¬e_scripts)
273 })
274 .await
275 }
276
277 async fn insert_block_header(
278 &self,
279 block_header: &BlockHeader,
280 nodes: &[(InOrderIndex, Word)],
281 has_client_notes: bool,
282 ) -> Result<(), StoreError> {
283 let block_header = block_header.clone();
284 let nodes = nodes.to_vec();
285 self.interact_with_connection(move |conn| {
286 SqliteStore::insert_block_header(conn, &block_header, &nodes, has_client_notes)
287 })
288 .await
289 }
290
291 async fn untrack_and_prune_irrelevant_blocks(
292 &self,
293 blocks_to_untrack: &[BlockNumber],
294 node_indices_to_remove: &[InOrderIndex],
295 ) -> Result<(), StoreError> {
296 let blocks_to_untrack = blocks_to_untrack.to_vec();
297 let node_indices_to_remove = node_indices_to_remove.to_vec();
298 self.interact_with_connection(move |conn| {
299 SqliteStore::prune_irrelevant_blocks(conn, &blocks_to_untrack, &node_indices_to_remove)
300 })
301 .await
302 }
303
304 async fn prune_account_history(
305 &self,
306 account_id: AccountId,
307 up_to_nonce: Felt,
308 ) -> Result<usize, StoreError> {
309 self.interact_with_connection(move |conn| {
310 SqliteStore::prune_account_history(conn, account_id, up_to_nonce)
311 })
312 .await
313 }
314
315 async fn get_block_headers(
316 &self,
317 block_numbers: &BTreeSet<BlockNumber>,
318 ) -> Result<Vec<(BlockHeader, BlockRelevance)>, StoreError> {
319 let block_numbers = block_numbers.clone();
320 Ok(self
321 .interact_with_connection(move |conn| {
322 SqliteStore::get_block_headers(conn, &block_numbers)
323 })
324 .await?)
325 }
326
327 async fn get_tracked_block_headers(&self) -> Result<Vec<BlockHeader>, StoreError> {
328 self.interact_with_connection(SqliteStore::get_tracked_block_headers).await
329 }
330
331 async fn get_tracked_block_header_numbers(&self) -> Result<BTreeSet<usize>, StoreError> {
332 self.interact_with_connection(SqliteStore::get_tracked_block_header_numbers)
333 .await
334 }
335
336 async fn get_partial_blockchain_nodes(
337 &self,
338 filter: PartialBlockchainFilter,
339 ) -> Result<BTreeMap<InOrderIndex, Word>, StoreError> {
340 self.interact_with_connection(move |conn| {
341 SqliteStore::get_partial_blockchain_nodes(conn, &filter)
342 })
343 .await
344 }
345
346 async fn get_current_blockchain_peaks(&self) -> Result<MmrPeaks, StoreError> {
347 self.interact_with_connection(SqliteStore::get_current_blockchain_peaks).await
348 }
349
350 async fn insert_account(
351 &self,
352 account: &Account,
353 initial_address: Address,
354 client_account_type: ClientAccountType,
355 ) -> Result<(), StoreError> {
356 let cloned_account = account.clone();
357
358 self.interact_with_connection(move |conn| {
359 SqliteStore::insert_account(
360 conn,
361 &cloned_account,
362 &initial_address,
363 client_account_type,
364 )
365 })
366 .await
367 }
368
369 async fn update_account(&self, account: &Account) -> Result<(), StoreError> {
370 let cloned_account = account.clone();
371
372 self.interact_with_connection(move |conn| {
373 SqliteStore::update_account(conn, &cloned_account)
374 })
375 .await
376 }
377
378 async fn get_account_ids(&self) -> Result<Vec<AccountId>, StoreError> {
379 self.interact_with_connection(SqliteStore::get_account_ids).await
380 }
381
382 async fn get_account_headers(&self) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError> {
383 self.interact_with_connection(SqliteStore::get_account_headers).await
384 }
385
386 async fn get_account_header(
387 &self,
388 account_id: AccountId,
389 ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError> {
390 self.interact_with_connection(move |conn| SqliteStore::get_account_header(conn, account_id))
391 .await
392 }
393
394 async fn get_account_header_by_commitment(
395 &self,
396 account_commitment: Word,
397 ) -> Result<Option<AccountHeader>, StoreError> {
398 self.interact_with_connection(move |conn| {
399 SqliteStore::get_account_header_by_commitment(conn, account_commitment)
400 })
401 .await
402 }
403
404 async fn get_account(
405 &self,
406 account_id: AccountId,
407 ) -> Result<Option<AccountRecord>, StoreError> {
408 self.interact_with_connection(move |conn| SqliteStore::get_account(conn, account_id))
409 .await
410 }
411
412 async fn get_account_code(
413 &self,
414 account_id: AccountId,
415 ) -> Result<Option<AccountCode>, StoreError> {
416 self.interact_with_connection(move |conn| {
417 SqliteStore::get_account_code_by_id(conn, account_id)
418 })
419 .await
420 }
421
422 async fn upsert_foreign_account_code(
423 &self,
424 account_id: AccountId,
425 code: AccountCode,
426 ) -> Result<(), StoreError> {
427 self.interact_with_connection(move |conn| {
428 SqliteStore::upsert_foreign_account_code(conn, account_id, &code)
429 })
430 .await
431 }
432
433 async fn get_foreign_account_code(
434 &self,
435 account_ids: Vec<AccountId>,
436 ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError> {
437 self.interact_with_connection(move |conn| {
438 SqliteStore::get_foreign_account_code(conn, account_ids)
439 })
440 .await
441 }
442
443 async fn set_setting(
444 &self,
445 scope: SettingScope,
446 key: String,
447 value: Vec<u8>,
448 ) -> Result<(), StoreError> {
449 self.interact_with_connection(move |conn| {
450 SqliteStore::set_setting(conn, scope, &key, &value).into_store_error()
451 })
452 .await
453 }
454
455 async fn get_setting(
456 &self,
457 scope: SettingScope,
458 key: String,
459 ) -> Result<Option<Vec<u8>>, StoreError> {
460 self.interact_with_connection(move |conn| SqliteStore::get_setting(conn, scope, &key))
461 .await
462 }
463
464 async fn remove_setting(&self, scope: SettingScope, key: String) -> Result<bool, StoreError> {
465 self.interact_with_connection(move |conn| SqliteStore::remove_setting(conn, scope, &key))
466 .await
467 }
468
469 async fn list_setting_keys(&self, scope: SettingScope) -> Result<Vec<String>, StoreError> {
470 self.interact_with_connection(move |conn| SqliteStore::list_setting_keys(conn, scope))
471 .await
472 }
473
474 async fn apply_settings_mutations(
475 &self,
476 scope: SettingScope,
477 mutations: Vec<SettingMutation>,
478 ) -> Result<(), StoreError> {
479 self.interact_with_connection(move |conn| {
480 let tx = conn.transaction().into_store_error()?;
481 for mutation in &mutations {
482 match mutation {
483 SettingMutation::Set { key, value } => {
484 SqliteStore::set_setting(&tx, scope, key, value).into_store_error()?;
485 },
486 SettingMutation::Remove { key } => {
487 SqliteStore::remove_setting(&tx, scope, key)?;
488 },
489 }
490 }
491 tx.commit().into_store_error()?;
492 Ok(())
493 })
494 .await
495 }
496
497 async fn get_unspent_input_note_nullifiers(&self) -> Result<Vec<Nullifier>, StoreError> {
498 self.interact_with_connection(SqliteStore::get_unspent_input_note_nullifiers)
499 .await
500 }
501
502 async fn get_account_vault(&self, account_id: AccountId) -> Result<AssetVault, StoreError> {
503 self.interact_with_connection(move |conn| SqliteStore::get_account_vault(conn, account_id))
504 .await
505 }
506
507 async fn get_account_assets(&self, account_id: AccountId) -> Result<Vec<Asset>, StoreError> {
508 self.interact_with_connection(move |conn| query_vault_assets(conn, account_id))
509 .await
510 }
511
512 async fn get_vault_asset_witnesses(
513 &self,
514 account_id: AccountId,
515 vault_root: Word,
516 asset_ids: BTreeSet<AssetId>,
517 ) -> Result<Vec<AssetWitness>, StoreError> {
518 self.interact_with_connection(move |conn| {
519 SqliteStore::get_vault_asset_witnesses(conn, account_id, vault_root, asset_ids)
520 })
521 .await
522 }
523
524 async fn get_account_asset(
525 &self,
526 account_id: AccountId,
527 asset_id: AssetId,
528 ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
529 self.interact_with_connection(move |conn| {
530 SqliteStore::get_account_asset(conn, account_id, asset_id)
531 })
532 .await
533 }
534
535 async fn get_account_map_item(
536 &self,
537 account_id: AccountId,
538 slot_name: StorageSlotName,
539 key: StorageMapKey,
540 ) -> Result<(Word, StorageMapWitness), StoreError> {
541 self.interact_with_connection(move |conn| {
542 SqliteStore::get_account_map_item(conn, account_id, slot_name, key)
543 })
544 .await
545 }
546
547 async fn get_account_storage(
548 &self,
549 account_id: AccountId,
550 filter: AccountStorageFilter,
551 ) -> Result<AccountStorage, StoreError> {
552 self.interact_with_connection(move |conn| {
553 SqliteStore::get_account_storage(conn, account_id, &filter)
554 })
555 .await
556 }
557
558 async fn get_addresses_by_account_id(
559 &self,
560 account_id: AccountId,
561 ) -> Result<Vec<Address>, StoreError> {
562 self.interact_with_connection(move |conn| {
563 SqliteStore::get_account_addresses(conn, account_id)
564 })
565 .await
566 }
567
568 async fn insert_address(
569 &self,
570 address: Address,
571 account_id: AccountId,
572 ) -> Result<(), StoreError> {
573 self.interact_with_connection(move |conn| {
574 let tx = conn.transaction().into_store_error()?;
575 SqliteStore::insert_address(&tx, &address, account_id)?;
576 tx.commit().into_store_error()
577 })
578 .await
579 }
580
581 async fn remove_address(&self, address: Address) -> Result<bool, StoreError> {
582 self.interact_with_connection(move |conn| SqliteStore::remove_address(conn, &address))
583 .await
584 }
585
586 async fn get_minimal_partial_account(
587 &self,
588 account_id: AccountId,
589 ) -> Result<Option<AccountRecord>, StoreError> {
590 self.interact_with_connection(move |conn| {
591 SqliteStore::get_minimal_partial_account(conn, account_id)
592 })
593 .await
594 }
595}
596
597fn database_error(err: impl core::fmt::Display) -> StoreError {
601 StoreError::DatabaseError(err.to_string())
602}
603
604pub(crate) fn current_timestamp_u64() -> u64 {
606 let now = chrono::Utc::now();
607 u64::try_from(now.timestamp()).expect("timestamp is always after epoch")
608}
609
610pub fn column_value_as_u64<I: rusqlite::RowIndex>(
615 row: &rusqlite::Row<'_>,
616 index: I,
617) -> rusqlite::Result<u64> {
618 let value: i64 = row.get(index)?;
619 #[allow(
620 clippy::cast_sign_loss,
621 reason = "We store u64 as i64 as sqlite only allows the latter."
622 )]
623 Ok(value as u64)
624}
625
626pub fn u64_to_value(v: u64) -> Value {
631 #[allow(
632 clippy::cast_possible_wrap,
633 reason = "We store u64 as i64 as sqlite only allows the latter."
634 )]
635 Value::Integer(v as i64)
636}
637
638#[cfg(test)]
642pub mod tests {
643 use std::boxed::Box;
644 use std::sync::LazyLock;
645
646 use miden_client::store::Store;
647 use miden_client::testing::common::create_test_store_path;
648
649 use super::db_management::migration::SqliteMigrator;
650 use super::db_management::migration::tests::damaging_migration;
651 use super::db_management::pool_manager::SqlitePoolManager;
652 use super::{Pool, SqliteStore};
653
654 static DAMAGING_MIGRATION: LazyLock<SqliteMigrator> = LazyLock::new(damaging_migration);
657
658 #[tokio::test]
659 async fn failed_migration_leaves_the_store_as_it_was() {
660 let database_filepath = create_test_store_path();
661 drop(SqliteStore::new(database_filepath.clone()).await.unwrap());
662
663 let pool = Pool::builder(SqlitePoolManager::new(database_filepath.clone()))
664 .build()
665 .unwrap();
666 let err = SqliteStore::migrate(&pool, &DAMAGING_MIGRATION).await.unwrap_err();
667
668 assert!(
669 err.to_string().contains("produced a schema this client does not expect"),
670 "the migration should have been rejected, got {err}"
671 );
672 SqliteStore::new(database_filepath).await.unwrap();
674 }
675
676 fn assert_send_sync<T: Send + Sync>() {}
677
678 #[test]
679 fn is_send_sync() {
680 assert_send_sync::<SqliteStore>();
681 assert_send_sync::<Box<dyn Store>>();
682 }
683
684 async fn dyn_trait_send_fut(store: Box<dyn Store>) {
686 let res = store.get_tracked_block_headers().await;
688 assert!(res.is_ok());
689 }
690
691 #[tokio::test]
692 async fn future_is_send() {
693 let client = SqliteStore::new(create_test_store_path()).await.unwrap();
694 let client: Box<SqliteStore> = client.into();
695 tokio::task::spawn(async move { dyn_trait_send_fut(client).await });
696 }
697
698 pub(crate) async fn create_test_store() -> SqliteStore {
699 SqliteStore::new(create_test_store_path()).await.unwrap()
700 }
701}