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