Skip to main content

miden_client_sqlite_store/account/
accounts.rs

1//! Account-related database operations.
2
3use std::collections::BTreeMap;
4use std::rc::Rc;
5use std::string::{String, ToString};
6use std::sync::{Arc, RwLock};
7use std::vec::Vec;
8
9use miden_client::account::{
10    Account,
11    AccountCode,
12    AccountDelta,
13    AccountHeader,
14    AccountId,
15    AccountIdPrefix,
16    AccountStorage,
17    Address,
18    PartialAccount,
19    PartialStorage,
20    PartialStorageMap,
21    StorageMap,
22    StorageSlotName,
23    StorageSlotType,
24};
25use miden_client::asset::{Asset, AssetVault, AssetWitness, FungibleAsset};
26use miden_client::store::{
27    AccountRecord,
28    AccountRecordData,
29    AccountStatus,
30    AccountStorageFilter,
31    StoreError,
32};
33use miden_client::sync::NoteTagRecord;
34use miden_client::utils::Serializable;
35use miden_client::{AccountError, Word};
36use miden_protocol::account::{AccountStorageHeader, StorageMapWitness, StorageSlotHeader};
37use miden_protocol::asset::{AssetVaultKey, PartialVault};
38use miden_protocol::crypto::merkle::MerkleError;
39use rusqlite::types::Value;
40use rusqlite::{Connection, Transaction, named_params, params};
41
42use crate::account::helpers::{
43    SerializedHeaderData,
44    parse_accounts,
45    query_account_addresses,
46    query_account_code,
47    query_account_headers,
48    query_storage_maps,
49    query_storage_slots,
50    query_storage_values,
51    query_vault_assets,
52};
53use crate::smt_forest::AccountSmtForest;
54use crate::sql_error::SqlResultExt;
55use crate::sync::{add_note_tag_tx, remove_note_tag_tx};
56use crate::{SqliteStore, column_value_as_u64, insert_sql, subst, u64_to_value};
57
58impl SqliteStore {
59    // READER METHODS
60    // --------------------------------------------------------------------------------------------
61
62    pub(crate) fn get_account_ids(conn: &mut Connection) -> Result<Vec<AccountId>, StoreError> {
63        const QUERY: &str = "SELECT id FROM tracked_accounts";
64
65        conn.prepare_cached(QUERY)
66            .into_store_error()?
67            .query_map([], |row| row.get(0))
68            .expect("no binding parameters used in query")
69            .map(|result| {
70                let id: String = result.map_err(|e| StoreError::ParsingError(e.to_string()))?;
71                Ok(AccountId::from_hex(&id).expect("account id is valid"))
72            })
73            .collect::<Result<Vec<AccountId>, StoreError>>()
74    }
75
76    pub(crate) fn get_account_headers(
77        conn: &mut Connection,
78    ) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError> {
79        const QUERY: &str = "
80            SELECT
81                a.id,
82                a.nonce,
83                a.vault_root,
84                a.storage_commitment,
85                a.code_commitment,
86                a.account_seed,
87                a.locked
88            FROM accounts AS a
89            JOIN (
90                SELECT id, MAX(nonce) AS nonce
91                FROM accounts
92                GROUP BY id
93            ) AS latest
94            ON a.id = latest.id
95            AND a.nonce = latest.nonce
96            ORDER BY a.id;
97            ";
98
99        conn.prepare_cached(QUERY)
100            .into_store_error()?
101            .query_map(params![], |row| {
102                let id: String = row.get(0)?;
103                let nonce: u64 = column_value_as_u64(row, 1)?;
104                let vault_root: String = row.get(2)?;
105                let storage_commitment: String = row.get(3)?;
106                let code_commitment: String = row.get(4)?;
107                let account_seed: Option<Vec<u8>> = row.get(5)?;
108                let locked: bool = row.get(6)?;
109
110                Ok(SerializedHeaderData {
111                    id,
112                    nonce,
113                    vault_root,
114                    storage_commitment,
115                    code_commitment,
116                    account_seed,
117                    locked,
118                })
119            })
120            .into_store_error()?
121            .map(|result| parse_accounts(result.into_store_error()?))
122            .collect::<Result<Vec<(AccountHeader, AccountStatus)>, StoreError>>()
123    }
124
125    pub(crate) fn get_account_header(
126        conn: &mut Connection,
127        account_id: AccountId,
128    ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError> {
129        Ok(query_account_headers(
130            conn,
131            "id = ? ORDER BY nonce DESC LIMIT 1",
132            params![account_id.to_hex()],
133        )?
134        .pop())
135    }
136
137    pub(crate) fn get_account_header_by_commitment(
138        conn: &mut Connection,
139        account_commitment: Word,
140    ) -> Result<Option<AccountHeader>, StoreError> {
141        let account_commitment_str: String = account_commitment.to_string();
142        Ok(
143            query_account_headers(conn, "account_commitment = ?", params![account_commitment_str])?
144                .pop()
145                .map(|(header, _)| header),
146        )
147    }
148
149    /// Retrieves a complete account record with full vault and storage data.
150    pub(crate) fn get_account(
151        conn: &mut Connection,
152        account_id: AccountId,
153    ) -> Result<Option<AccountRecord>, StoreError> {
154        let Some((header, status)) = Self::get_account_header(conn, account_id)? else {
155            return Ok(None);
156        };
157
158        let assets = query_vault_assets(conn, "root = ?", params![header.vault_root().to_hex()])?;
159        let vault = AssetVault::new(&assets)?;
160
161        let slots = query_storage_slots(
162            conn,
163            "commitment = ?",
164            params![header.storage_commitment().to_hex()],
165        )?
166        .into_values()
167        .collect();
168
169        let storage = AccountStorage::new(slots)?;
170
171        let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
172            return Ok(None);
173        };
174
175        let account = Account::new_unchecked(
176            header.id(),
177            vault,
178            storage,
179            account_code,
180            header.nonce(),
181            status.seed().copied(),
182        );
183
184        let addresses = query_account_addresses(conn, header.id())?;
185        let account_data = AccountRecordData::Full(account);
186        Ok(Some(AccountRecord::new(account_data, status, addresses)))
187    }
188
189    /// Retrieves a minimal partial account record with storage and vault witnesses.
190    pub(crate) fn get_minimal_partial_account(
191        conn: &mut Connection,
192        smt_forest: &Arc<RwLock<AccountSmtForest>>,
193        account_id: AccountId,
194    ) -> Result<Option<AccountRecord>, StoreError> {
195        let Some((header, status)) = Self::get_account_header(conn, account_id)? else {
196            return Ok(None);
197        };
198
199        // Partial vault retrieval
200        let partial_vault = PartialVault::new(header.vault_root());
201
202        // Partial storage retrieval
203        let mut storage_header = Vec::new();
204        let mut maps = vec![];
205
206        let storage_values = query_storage_values(
207            conn,
208            "commitment = ?",
209            params![header.storage_commitment().to_hex()],
210        )?;
211
212        // Collect all map roots for a single batched query
213        let map_roots: Vec<Value> = storage_values
214            .iter()
215            .filter(|(_, (slot_type, _))| *slot_type == StorageSlotType::Map)
216            .map(|(_, (_, value))| Value::from(value.to_hex()))
217            .collect();
218
219        // Fetch all storage maps in a single query
220        let mut all_storage_maps = if map_roots.is_empty() {
221            BTreeMap::new()
222        } else {
223            query_storage_maps(conn, "root IN rarray(?)", [Rc::new(map_roots)])?
224        };
225
226        for (slot_name, (slot_type, value)) in storage_values {
227            storage_header.push(StorageSlotHeader::new(slot_name.clone(), slot_type, value));
228            if slot_type == StorageSlotType::Map {
229                let mut partial_storage_map = PartialStorageMap::new(value);
230
231                if let Some(map) = all_storage_maps.remove(&value) {
232                    let smt_forest = smt_forest.read().expect("smt_forest read lock not poisoned");
233                    for (k, _v) in map.entries() {
234                        let witness = smt_forest.get_storage_map_item_witness(value, *k)?;
235                        partial_storage_map.add(witness).map_err(StoreError::MerkleStoreError)?;
236                    }
237                }
238
239                maps.push(partial_storage_map);
240            }
241        }
242        storage_header.sort_by_key(StorageSlotHeader::id);
243        let storage_header =
244            AccountStorageHeader::new(storage_header).map_err(StoreError::AccountError)?;
245        let partial_storage =
246            PartialStorage::new(storage_header, maps).map_err(StoreError::AccountError)?;
247
248        let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
249            return Ok(None);
250        };
251
252        let partial_account = PartialAccount::new(
253            header.id(),
254            header.nonce(),
255            account_code,
256            partial_storage,
257            partial_vault,
258            status.seed().copied(),
259        )?;
260        let account_record_data = AccountRecordData::Partial(partial_account);
261        let addresses = query_account_addresses(conn, header.id())?;
262        Ok(Some(AccountRecord::new(account_record_data, status, addresses)))
263    }
264
265    pub fn get_foreign_account_code(
266        conn: &mut Connection,
267        account_ids: Vec<AccountId>,
268    ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError> {
269        let params: Vec<Value> =
270            account_ids.into_iter().map(|id| Value::from(id.to_hex())).collect();
271        const QUERY: &str = "
272            SELECT account_id, code
273            FROM foreign_account_code JOIN account_code ON foreign_account_code.code_commitment = account_code.commitment
274            WHERE account_id IN rarray(?)";
275
276        conn.prepare_cached(QUERY)
277            .into_store_error()?
278            .query_map([Rc::new(params)], |row| Ok((row.get(0)?, row.get(1)?)))
279            .expect("no binding parameters used in query")
280            .map(|result| {
281                result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
282                    |(id, code): (String, Vec<u8>)| {
283                        Ok((
284                            AccountId::from_hex(&id).map_err(|err| {
285                                StoreError::AccountError(
286                                    AccountError::FinalAccountHeaderIdParsingFailed(err),
287                                )
288                            })?,
289                            AccountCode::from_bytes(&code).map_err(StoreError::AccountError)?,
290                        ))
291                    },
292                )
293            })
294            .collect::<Result<BTreeMap<AccountId, AccountCode>, _>>()
295    }
296
297    /// Retrieves the full asset vault for a specific account.
298    pub fn get_account_vault(
299        conn: &Connection,
300        account_id: AccountId,
301    ) -> Result<AssetVault, StoreError> {
302        let assets = query_vault_assets(
303            conn,
304            "root = (SELECT vault_root FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1)",
305            params![account_id.to_hex()],
306        )?;
307
308        Ok(AssetVault::new(&assets)?)
309    }
310
311    /// Retrieves the full storage for a specific account.
312    pub fn get_account_storage(
313        conn: &Connection,
314        account_id: AccountId,
315        filter: &AccountStorageFilter,
316    ) -> Result<AccountStorage, StoreError> {
317        let (where_clause, params) = match filter {
318            AccountStorageFilter::All => (
319                "commitment = (SELECT storage_commitment FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1)",
320                params![account_id.to_hex()],
321            ),
322            AccountStorageFilter::Root(root) => (
323                "commitment = (SELECT storage_commitment FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1) AND slot_value = ?",
324                params![account_id.to_hex(), root.to_hex()],
325            ),
326            AccountStorageFilter::SlotName(slot_name) => (
327                "commitment = (SELECT storage_commitment FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1) AND slot_name = ?",
328                params![account_id.to_hex(), slot_name.to_string()],
329            ),
330        };
331
332        let slots = query_storage_slots(conn, where_clause, params)?.into_values().collect();
333
334        Ok(AccountStorage::new(slots)?)
335    }
336
337    /// Fetches a specific asset from the account's vault without the need of loading the entire
338    /// vault. The witness is retrieved from the [`AccountSmtForest`].
339    pub(crate) fn get_account_asset(
340        conn: &mut Connection,
341        smt_forest: &Arc<RwLock<AccountSmtForest>>,
342        account_id: AccountId,
343        vault_key: AssetVaultKey,
344    ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
345        let header = Self::get_account_header(conn, account_id)?
346            .ok_or(StoreError::AccountDataNotFound(account_id))?
347            .0;
348
349        let smt_forest = smt_forest.read().expect("smt_forest read lock not poisoned");
350        match smt_forest.get_asset_and_witness(header.vault_root(), vault_key) {
351            Ok((asset, witness)) => Ok(Some((asset, witness))),
352            Err(StoreError::MerkleStoreError(MerkleError::UntrackedKey(_))) => Ok(None),
353            Err(err) => Err(err),
354        }
355    }
356
357    /// Retrieves a specific item from the account's storage map without loading the entire storage.
358    /// The witness is retrieved from the [`AccountSmtForest`].
359    pub(crate) fn get_account_map_item(
360        conn: &mut Connection,
361        smt_forest: &Arc<RwLock<AccountSmtForest>>,
362        account_id: AccountId,
363        slot_name: StorageSlotName,
364        key: Word,
365    ) -> Result<(Word, StorageMapWitness), StoreError> {
366        let header = Self::get_account_header(conn, account_id)?
367            .ok_or(StoreError::AccountDataNotFound(account_id))?
368            .0;
369
370        let mut storage_values = query_storage_values(
371            conn,
372            "commitment = ? AND slot_name = ?",
373            params![header.storage_commitment().to_hex(), slot_name.to_string()],
374        )?;
375        let (slot_type, map_root) = storage_values
376            .remove(&slot_name)
377            .ok_or(StoreError::AccountStorageRootNotFound(header.storage_commitment()))?;
378        if slot_type != StorageSlotType::Map {
379            return Err(StoreError::AccountError(AccountError::StorageSlotNotMap(slot_name)));
380        }
381
382        let smt_forest = smt_forest.read().expect("smt_forest read lock not poisoned");
383        let witness = smt_forest.get_storage_map_item_witness(map_root, key)?;
384        let item = witness.get(&key).unwrap_or(miden_client::EMPTY_WORD);
385
386        Ok((item, witness))
387    }
388
389    pub(crate) fn get_account_addresses(
390        conn: &mut Connection,
391        account_id: AccountId,
392    ) -> Result<Vec<Address>, StoreError> {
393        query_account_addresses(conn, account_id)
394    }
395
396    // MUTATOR/WRITER METHODS
397    // --------------------------------------------------------------------------------------------
398
399    pub(crate) fn insert_account(
400        conn: &mut Connection,
401        smt_forest: &Arc<RwLock<AccountSmtForest>>,
402        account: &Account,
403        initial_address: &Address,
404    ) -> Result<(), StoreError> {
405        let tx = conn.transaction().into_store_error()?;
406
407        Self::insert_account_code(&tx, account.code())?;
408
409        Self::insert_storage_slots(
410            &tx,
411            account.storage().to_commitment(),
412            account.storage().slots().iter(),
413        )?;
414
415        Self::insert_assets(&tx, account.vault().root(), account.vault().assets())?;
416        Self::insert_account_header(&tx, &account.into(), account.seed())?;
417
418        Self::insert_address(&tx, initial_address, account.id())?;
419
420        tx.commit().into_store_error()?;
421
422        let mut smt_forest = smt_forest.write().expect("smt_forest write lock not poisoned");
423        smt_forest.insert_account_state(account.vault(), account.storage())?;
424
425        Ok(())
426    }
427
428    pub(crate) fn update_account(
429        conn: &mut Connection,
430        smt_forest: &Arc<RwLock<AccountSmtForest>>,
431        new_account_state: &Account,
432    ) -> Result<(), StoreError> {
433        const QUERY: &str = "SELECT id FROM accounts WHERE id = ?";
434        if conn
435            .prepare(QUERY)
436            .into_store_error()?
437            .query_map(params![new_account_state.id().to_hex()], |row| row.get(0))
438            .into_store_error()?
439            .map(|result| {
440                result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
441                    |id: String| {
442                        AccountId::from_hex(&id).map_err(|err| {
443                            StoreError::AccountError(
444                                AccountError::FinalAccountHeaderIdParsingFailed(err),
445                            )
446                        })
447                    },
448                )
449            })
450            .next()
451            .is_none()
452        {
453            return Err(StoreError::AccountDataNotFound(new_account_state.id()));
454        }
455
456        let mut smt_forest = smt_forest.write().expect("smt_forest write lock not poisoned");
457        let tx = conn.transaction().into_store_error()?;
458        Self::update_account_state(&tx, &mut smt_forest, new_account_state)?;
459        tx.commit().into_store_error()
460    }
461
462    pub fn upsert_foreign_account_code(
463        conn: &mut Connection,
464        account_id: AccountId,
465        code: &AccountCode,
466    ) -> Result<(), StoreError> {
467        let tx = conn.transaction().into_store_error()?;
468
469        Self::insert_account_code(&tx, code)?;
470
471        const QUERY: &str =
472            insert_sql!(foreign_account_code { account_id, code_commitment } | REPLACE);
473
474        tx.execute(QUERY, params![account_id.to_hex(), code.commitment().to_string()])
475            .into_store_error()?;
476
477        Self::insert_account_code(&tx, code)?;
478        tx.commit().into_store_error()
479    }
480
481    pub(crate) fn insert_address(
482        tx: &Transaction<'_>,
483        address: &Address,
484        account_id: AccountId,
485    ) -> Result<(), StoreError> {
486        let derived_note_tag = address.to_note_tag();
487        let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
488
489        add_note_tag_tx(tx, &note_tag_record)?;
490        Self::insert_address_internal(tx, address, account_id)?;
491
492        Ok(())
493    }
494
495    pub(crate) fn remove_address(
496        conn: &mut Connection,
497        address: &Address,
498        account_id: AccountId,
499    ) -> Result<(), StoreError> {
500        let derived_note_tag = address.to_note_tag();
501        let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
502
503        let tx = conn.transaction().into_store_error()?;
504        remove_note_tag_tx(&tx, note_tag_record)?;
505        Self::remove_address_internal(&tx, address)?;
506
507        tx.commit().into_store_error()
508    }
509
510    /// Inserts an [`AccountCode`].
511    pub(crate) fn insert_account_code(
512        tx: &Transaction<'_>,
513        account_code: &AccountCode,
514    ) -> Result<(), StoreError> {
515        const QUERY: &str = insert_sql!(account_code { commitment, code } | IGNORE);
516        tx.execute(QUERY, params![account_code.commitment().to_hex(), account_code.to_bytes()])
517            .into_store_error()?;
518        Ok(())
519    }
520
521    /// Applies the account delta to the account state, updating the vault and storage maps.
522    ///
523    /// The apply delta operation strats by copying over the initial account state (vault and
524    /// storage) and then applying the delta on top of it. The storage and vault elements are
525    /// overwritten in the new state. In the cases where the delta depends on previous state (e.g.
526    /// adding or subtracting fungible assets), the previous state needs to be provided via the
527    /// `updated_fungible_assets` and `updated_storage_maps` parameters.
528    pub(crate) fn apply_account_delta(
529        tx: &Transaction<'_>,
530        smt_forest: &mut AccountSmtForest,
531        init_account_state: &AccountHeader,
532        final_account_state: &AccountHeader,
533        updated_fungible_assets: BTreeMap<AccountIdPrefix, FungibleAsset>,
534        updated_storage_maps: BTreeMap<StorageSlotName, StorageMap>,
535        delta: &AccountDelta,
536    ) -> Result<(), StoreError> {
537        // Copy over the storage and vault from the previous state. Non-relevant data will not be
538        // modified.
539        Self::copy_account_state(tx, init_account_state, final_account_state)?;
540
541        Self::apply_account_vault_delta(
542            tx,
543            smt_forest,
544            init_account_state,
545            final_account_state,
546            updated_fungible_assets,
547            delta,
548        )?;
549
550        let updated_storage_slots =
551            Self::apply_account_storage_delta(smt_forest, updated_storage_maps, delta)?;
552
553        Self::insert_storage_slots(
554            tx,
555            final_account_state.storage_commitment(),
556            updated_storage_slots.values(),
557        )?;
558
559        Ok(())
560    }
561
562    /// Removes account states with the specified hashes from the database and pops their
563    /// SMT roots from the forest to free up memory.
564    ///
565    /// This is used to rollback account changes when a transaction is discarded,
566    /// effectively undoing the account state changes that were applied by the transaction.
567    ///
568    /// Note: This is not part of the Store trait and is only used internally by the `SQLite` store
569    /// implementation to handle transaction rollbacks.
570    pub(crate) fn undo_account_state(
571        tx: &Transaction<'_>,
572        smt_forest: &mut AccountSmtForest,
573        account_commitments: &[Word],
574    ) -> Result<(), StoreError> {
575        if account_commitments.is_empty() {
576            return Ok(());
577        }
578
579        let account_hash_params = Rc::new(
580            account_commitments.iter().map(|h| Value::from(h.to_hex())).collect::<Vec<_>>(),
581        );
582
583        // Query all SMT roots before deletion so we can pop them from the forest
584        let smt_roots = Self::get_smt_roots_by_account_commitment(tx, &account_hash_params)?;
585
586        const DELETE_QUERY: &str = "DELETE FROM accounts WHERE account_commitment IN rarray(?)";
587        tx.execute(DELETE_QUERY, params![account_hash_params]).into_store_error()?;
588
589        // Pop the roots from the forest to release memory for nodes that are no longer reachable
590        smt_forest.pop_roots(smt_roots);
591
592        Ok(())
593    }
594
595    /// Updates the account state in the database to a new complete account state.
596    ///
597    /// This function replaces the current account state with a completely new one. It:
598    /// - Inserts the new account header
599    /// - Stores all storage slots and their maps
600    /// - Stores all vault assets
601    /// - Updates the SMT forest with the new state
602    /// - Pops old SMT roots from the forest to free memory
603    ///
604    /// This is typically used for replacing a full account state, as opposed to applying
605    /// incremental deltas via `apply_account_delta`.
606    ///
607    /// # Arguments
608    /// * `tx` - Database transaction
609    /// * `smt_forest` - SMT forest for updating and pruning
610    /// * `new_account_state` - The new complete account state to persist
611    ///
612    /// # Returns
613    /// `Ok(())` if the update was successful, or an error if any operation fails.
614    pub(crate) fn update_account_state(
615        tx: &Transaction<'_>,
616        smt_forest: &mut AccountSmtForest,
617        new_account_state: &Account,
618    ) -> Result<(), StoreError> {
619        // Get old SMT roots before updating so we can prune them after
620        let old_roots = Self::get_smt_roots_by_account_id(tx, new_account_state.id())?;
621
622        smt_forest.insert_account_state(new_account_state.vault(), new_account_state.storage())?;
623        Self::insert_storage_slots(
624            tx,
625            new_account_state.storage().to_commitment(),
626            new_account_state.storage().slots().iter(),
627        )?;
628        Self::insert_assets(
629            tx,
630            new_account_state.vault().root(),
631            new_account_state.vault().assets(),
632        )?;
633        Self::insert_account_header(tx, &new_account_state.into(), None)?;
634
635        // Pop old roots to free memory for nodes no longer reachable
636        smt_forest.pop_roots(old_roots);
637
638        Ok(())
639    }
640
641    /// Locks the account if the mismatched digest doesn't belong to a previous account state (stale
642    /// data).
643    pub(crate) fn lock_account_on_unexpected_commitment(
644        tx: &Transaction<'_>,
645        account_id: &AccountId,
646        mismatched_digest: &Word,
647    ) -> Result<(), StoreError> {
648        // Mismatched digests may be due to stale network data. If the mismatched digest is
649        // tracked in the db and corresponds to the mismatched account, it means we
650        // got a past update and shouldn't lock the account.
651        const QUERY: &str = "UPDATE accounts SET locked = true WHERE id = :account_id AND NOT EXISTS (SELECT 1 FROM accounts WHERE id = :account_id AND account_commitment = :digest)";
652        tx.execute(
653            QUERY,
654            named_params! {
655                ":account_id": account_id.to_hex(),
656                ":digest": mismatched_digest.to_string()
657            },
658        )
659        .into_store_error()?;
660        Ok(())
661    }
662
663    // HELPERS
664    // --------------------------------------------------------------------------------------------
665
666    /// Inserts the new final account header and copies over the previous account state.
667    fn copy_account_state(
668        tx: &Transaction<'_>,
669        init_account_header: &AccountHeader,
670        final_account_header: &AccountHeader,
671    ) -> Result<(), StoreError> {
672        Self::insert_account_header(tx, final_account_header, None)?;
673
674        if init_account_header.vault_root() != final_account_header.vault_root() {
675            const VAULT_QUERY: &str = "
676                INSERT OR IGNORE INTO account_assets (
677                    root,
678                    vault_key,
679                    faucet_id_prefix,
680                    asset
681                )
682                SELECT
683                    ?, --new root
684                    vault_key,
685                    faucet_id_prefix,
686                    asset
687                FROM account_assets
688                WHERE root = (SELECT vault_root FROM accounts WHERE account_commitment = ?)
689                ";
690            tx.execute(
691                VAULT_QUERY,
692                params![
693                    final_account_header.vault_root().to_hex(),
694                    init_account_header.commitment().to_hex()
695                ],
696            )
697            .into_store_error()?;
698        }
699
700        if init_account_header.storage_commitment() != final_account_header.storage_commitment() {
701            const STORAGE_QUERY: &str = "
702                INSERT OR IGNORE INTO account_storage (
703                    commitment,
704                    slot_name,
705                    slot_value,
706                    slot_type
707                )
708                SELECT
709                    ?, -- new commitment
710                    slot_name,
711                    slot_value,
712                    slot_type
713                FROM account_storage
714                WHERE commitment = (SELECT storage_commitment FROM accounts WHERE account_commitment = ?)
715                ";
716
717            tx.execute(
718                STORAGE_QUERY,
719                params![
720                    final_account_header.storage_commitment().to_hex(),
721                    init_account_header.commitment().to_hex()
722                ],
723            )
724            .into_store_error()?;
725        }
726
727        Ok(())
728    }
729
730    /// Returns all SMT roots for a given account ID's latest state.
731    ///
732    /// This function retrieves all Merkle tree roots needed for the SMT forest, including:
733    /// - The vault root for all asset nodes
734    /// - All storage map roots for storage slot map nodes
735    fn get_smt_roots_by_account_id(
736        tx: &Transaction<'_>,
737        account_id: AccountId,
738    ) -> Result<Vec<Word>, StoreError> {
739        const LATEST_ACCOUNT_QUERY: &str = r"
740        SELECT vault_root, storage_commitment
741        FROM accounts
742        WHERE id = ?1
743        ORDER BY nonce DESC
744        LIMIT 1
745    ";
746
747        const STORAGE_MAP_ROOTS_QUERY: &str = r"
748        SELECT slot_value
749        FROM account_storage
750        WHERE commitment = ?1
751          AND slot_type = ?2
752          AND slot_value IS NOT NULL
753    ";
754
755        let map_slot_type = StorageSlotType::Map as u8;
756
757        // 1) Fetch latest vault root + storage commitment.
758        let (vault_root, storage_commitment): (String, String) = tx
759            .query_row(LATEST_ACCOUNT_QUERY, params![account_id.to_hex()], |row| {
760                Ok((row.get(0)?, row.get(1)?))
761            })
762            .into_store_error()?;
763
764        let mut roots = Vec::new();
765
766        // Always include the vault root.
767        if let Ok(root) = Word::try_from(vault_root.as_str()) {
768            roots.push(root);
769        }
770
771        // 2) Fetch storage map roots for the latest storage commitment.
772        let mut stmt = tx.prepare(STORAGE_MAP_ROOTS_QUERY).into_store_error()?;
773        let iter = stmt
774            .query_map(params![storage_commitment, map_slot_type], |row| row.get::<_, String>(0))
775            .into_store_error()?;
776
777        roots.extend(iter.filter_map(Result::ok).filter_map(|r| Word::try_from(r.as_str()).ok()));
778
779        Ok(roots)
780    }
781
782    /// Returns all SMT roots (vault root + storage map roots) for the given account commitments.
783    fn get_smt_roots_by_account_commitment(
784        tx: &Transaction<'_>,
785        account_hash_params: &Rc<Vec<Value>>,
786    ) -> Result<Vec<Word>, StoreError> {
787        const ROOTS_QUERY: &str = "
788            SELECT vault_root FROM accounts WHERE account_commitment IN rarray(?1)
789            UNION ALL
790            SELECT slot_value FROM account_storage
791            WHERE commitment IN (
792                SELECT storage_commitment FROM accounts WHERE account_commitment IN rarray(?1)
793            ) AND slot_type = ?2";
794
795        let map_slot_type = StorageSlotType::Map as u8;
796        let mut stmt = tx.prepare(ROOTS_QUERY).into_store_error()?;
797        let roots = stmt
798            .query_map(params![account_hash_params, map_slot_type], |row| row.get::<_, String>(0))
799            .into_store_error()?
800            .filter_map(Result::ok)
801            .filter_map(|r| Word::try_from(r.as_str()).ok())
802            .collect();
803
804        Ok(roots)
805    }
806
807    /// Inserts a new account record into the database.
808    fn insert_account_header(
809        tx: &Transaction<'_>,
810        account: &AccountHeader,
811        account_seed: Option<Word>,
812    ) -> Result<(), StoreError> {
813        let id: String = account.id().to_hex();
814        let code_commitment = account.code_commitment().to_string();
815        let storage_commitment = account.storage_commitment().to_string();
816        let vault_root = account.vault_root().to_string();
817        let nonce = u64_to_value(account.nonce().as_int());
818        let commitment = account.commitment().to_string();
819
820        let account_seed = account_seed.map(|seed| seed.to_bytes());
821
822        const QUERY: &str = insert_sql!(
823            accounts {
824                id,
825                code_commitment,
826                storage_commitment,
827                vault_root,
828                nonce,
829                account_seed,
830                account_commitment,
831                locked
832            } | REPLACE
833        );
834
835        tx.execute(
836            QUERY,
837            params![
838                id,
839                code_commitment,
840                storage_commitment,
841                vault_root,
842                nonce,
843                account_seed,
844                commitment,
845                false,
846            ],
847        )
848        .into_store_error()?;
849
850        Self::insert_tracked_account_id_tx(tx, account.id())?;
851        Ok(())
852    }
853
854    fn insert_tracked_account_id_tx(
855        tx: &Transaction<'_>,
856        account_id: AccountId,
857    ) -> Result<(), StoreError> {
858        const QUERY: &str = insert_sql!(tracked_accounts { id } | IGNORE);
859        tx.execute(QUERY, params![account_id.to_hex()]).into_store_error()?;
860        Ok(())
861    }
862
863    fn insert_address_internal(
864        tx: &Transaction<'_>,
865        address: &Address,
866        account_id: AccountId,
867    ) -> Result<(), StoreError> {
868        const QUERY: &str = insert_sql!(addresses { address, account_id } | REPLACE);
869        let serialized_address = address.to_bytes();
870        tx.execute(QUERY, params![serialized_address, account_id.to_hex(),])
871            .into_store_error()?;
872
873        Ok(())
874    }
875
876    fn remove_address_internal(tx: &Transaction<'_>, address: &Address) -> Result<(), StoreError> {
877        let serialized_address = address.to_bytes();
878
879        const DELETE_QUERY: &str = "DELETE FROM addresses WHERE address = ?";
880        tx.execute(DELETE_QUERY, params![serialized_address]).into_store_error()?;
881
882        Ok(())
883    }
884}