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
167        let storage = AccountStorage::new(slots)?;
168
169        let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
170            return Ok(None);
171        };
172
173        let account = Account::new_unchecked(
174            header.id(),
175            vault,
176            storage,
177            account_code,
178            header.nonce(),
179            status.seed().copied(),
180        );
181
182        let addresses = query_account_addresses(conn, header.id())?;
183        let account_data = AccountRecordData::Full(account);
184        Ok(Some(AccountRecord::new(account_data, status, addresses)))
185    }
186
187    /// Retrieves a minimal partial account record with storage and vault witnesses.
188    pub(crate) fn get_minimal_partial_account(
189        conn: &mut Connection,
190        smt_forest: &Arc<RwLock<AccountSmtForest>>,
191        account_id: AccountId,
192    ) -> Result<Option<AccountRecord>, StoreError> {
193        let Some((header, status)) = Self::get_account_header(conn, account_id)? else {
194            return Ok(None);
195        };
196
197        // Partial vault retrieval
198        let partial_vault = PartialVault::new(header.vault_root());
199
200        // Partial storage retrieval
201        let mut storage_header = Vec::new();
202        let mut maps = vec![];
203
204        let storage_values = query_storage_values(
205            conn,
206            "commitment = ?",
207            params![header.storage_commitment().to_hex()],
208        )?;
209
210        // Collect all map roots for a single batched query
211        let map_roots: Vec<Value> = storage_values
212            .iter()
213            .filter(|(_, (slot_type, _))| *slot_type == StorageSlotType::Map)
214            .map(|(_, (_, value))| Value::from(value.to_hex()))
215            .collect();
216
217        // Fetch all storage maps in a single query
218        let mut all_storage_maps = if map_roots.is_empty() {
219            BTreeMap::new()
220        } else {
221            query_storage_maps(conn, "root IN rarray(?)", [Rc::new(map_roots)])?
222        };
223
224        for (slot_name, (slot_type, value)) in storage_values {
225            storage_header.push(StorageSlotHeader::new(slot_name.clone(), slot_type, value));
226            if slot_type == StorageSlotType::Map {
227                let mut partial_storage_map = PartialStorageMap::new(value);
228
229                if let Some(map) = all_storage_maps.remove(&value) {
230                    let smt_forest = smt_forest.read().expect("smt_forest read lock not poisoned");
231                    for (k, _v) in map.entries() {
232                        let witness = smt_forest.get_storage_map_item_witness(value, *k)?;
233                        partial_storage_map.add(witness).map_err(StoreError::MerkleStoreError)?;
234                    }
235                }
236
237                maps.push(partial_storage_map);
238            }
239        }
240        storage_header.sort_by_key(StorageSlotHeader::id);
241        let storage_header =
242            AccountStorageHeader::new(storage_header).map_err(StoreError::AccountError)?;
243        let partial_storage =
244            PartialStorage::new(storage_header, maps).map_err(StoreError::AccountError)?;
245
246        let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
247            return Ok(None);
248        };
249
250        let partial_account = PartialAccount::new(
251            header.id(),
252            header.nonce(),
253            account_code,
254            partial_storage,
255            partial_vault,
256            status.seed().copied(),
257        )?;
258        let account_record_data = AccountRecordData::Partial(partial_account);
259        let addresses = query_account_addresses(conn, header.id())?;
260        Ok(Some(AccountRecord::new(account_record_data, status, addresses)))
261    }
262
263    pub fn get_foreign_account_code(
264        conn: &mut Connection,
265        account_ids: Vec<AccountId>,
266    ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError> {
267        let params: Vec<Value> =
268            account_ids.into_iter().map(|id| Value::from(id.to_hex())).collect();
269        const QUERY: &str = "
270            SELECT account_id, code
271            FROM foreign_account_code JOIN account_code ON foreign_account_code.code_commitment = account_code.commitment
272            WHERE account_id IN rarray(?)";
273
274        conn.prepare_cached(QUERY)
275            .into_store_error()?
276            .query_map([Rc::new(params)], |row| Ok((row.get(0)?, row.get(1)?)))
277            .expect("no binding parameters used in query")
278            .map(|result| {
279                result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
280                    |(id, code): (String, Vec<u8>)| {
281                        Ok((
282                            AccountId::from_hex(&id).map_err(|err| {
283                                StoreError::AccountError(
284                                    AccountError::FinalAccountHeaderIdParsingFailed(err),
285                                )
286                            })?,
287                            AccountCode::from_bytes(&code).map_err(StoreError::AccountError)?,
288                        ))
289                    },
290                )
291            })
292            .collect::<Result<BTreeMap<AccountId, AccountCode>, _>>()
293    }
294
295    /// Retrieves the full asset vault for a specific account.
296    pub fn get_account_vault(
297        conn: &Connection,
298        account_id: AccountId,
299    ) -> Result<AssetVault, StoreError> {
300        let assets = query_vault_assets(
301            conn,
302            "root = (SELECT vault_root FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1)",
303            params![account_id.to_hex()],
304        )?;
305
306        Ok(AssetVault::new(&assets)?)
307    }
308
309    /// Retrieves the full storage for a specific account.
310    pub fn get_account_storage(
311        conn: &Connection,
312        account_id: AccountId,
313        filter: &AccountStorageFilter,
314    ) -> Result<AccountStorage, StoreError> {
315        let (where_clause, params) = match filter {
316            AccountStorageFilter::All => (
317                "commitment = (SELECT storage_commitment FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1)",
318                params![account_id.to_hex()],
319            ),
320            AccountStorageFilter::Root(root) => (
321                "commitment = (SELECT storage_commitment FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1) AND slot_value = ?",
322                params![account_id.to_hex(), root.to_hex()],
323            ),
324            AccountStorageFilter::SlotName(slot_name) => (
325                "commitment = (SELECT storage_commitment FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1) AND slot_name = ?",
326                params![account_id.to_hex(), slot_name.to_string()],
327            ),
328        };
329
330        let slots = query_storage_slots(conn, where_clause, params)?;
331
332        Ok(AccountStorage::new(slots)?)
333    }
334
335    /// Fetches a specific asset from the account's vault without the need of loading the entire
336    /// vault. The witness is retrieved from the [`AccountSmtForest`].
337    pub(crate) fn get_account_asset(
338        conn: &mut Connection,
339        smt_forest: &Arc<RwLock<AccountSmtForest>>,
340        account_id: AccountId,
341        vault_key: AssetVaultKey,
342    ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
343        let header = Self::get_account_header(conn, account_id)?
344            .ok_or(StoreError::AccountDataNotFound(account_id))?
345            .0;
346
347        let smt_forest = smt_forest.read().expect("smt_forest read lock not poisoned");
348        match smt_forest.get_asset_and_witness(header.vault_root(), vault_key) {
349            Ok((asset, witness)) => Ok(Some((asset, witness))),
350            Err(StoreError::MerkleStoreError(MerkleError::UntrackedKey(_))) => Ok(None),
351            Err(err) => Err(err),
352        }
353    }
354
355    /// Retrieves a specific item from the account's storage map without loading the entire storage.
356    /// The witness is retrieved from the [`AccountSmtForest`].
357    pub(crate) fn get_account_map_item(
358        conn: &mut Connection,
359        smt_forest: &Arc<RwLock<AccountSmtForest>>,
360        account_id: AccountId,
361        slot_name: StorageSlotName,
362        key: Word,
363    ) -> Result<(Word, StorageMapWitness), StoreError> {
364        let header = Self::get_account_header(conn, account_id)?
365            .ok_or(StoreError::AccountDataNotFound(account_id))?
366            .0;
367
368        let mut storage_values = query_storage_values(
369            conn,
370            "commitment = ? AND slot_name = ?",
371            params![header.storage_commitment().to_hex(), slot_name.to_string()],
372        )?;
373        let (slot_type, map_root) = storage_values
374            .remove(&slot_name)
375            .ok_or(StoreError::AccountStorageRootNotFound(header.storage_commitment()))?;
376        if slot_type != StorageSlotType::Map {
377            return Err(StoreError::AccountError(AccountError::StorageSlotNotMap(slot_name)));
378        }
379
380        let smt_forest = smt_forest.read().expect("smt_forest read lock not poisoned");
381        let witness = smt_forest.get_storage_map_item_witness(map_root, key)?;
382        let item = witness.get(&key).unwrap_or(miden_client::EMPTY_WORD);
383
384        Ok((item, witness))
385    }
386
387    pub(crate) fn get_account_addresses(
388        conn: &mut Connection,
389        account_id: AccountId,
390    ) -> Result<Vec<Address>, StoreError> {
391        query_account_addresses(conn, account_id)
392    }
393
394    // MUTATOR/WRITER METHODS
395    // --------------------------------------------------------------------------------------------
396
397    pub(crate) fn insert_account(
398        conn: &mut Connection,
399        smt_forest: &Arc<RwLock<AccountSmtForest>>,
400        account: &Account,
401        initial_address: &Address,
402    ) -> Result<(), StoreError> {
403        let tx = conn.transaction().into_store_error()?;
404
405        Self::insert_account_code(&tx, account.code())?;
406
407        Self::insert_storage_slots(
408            &tx,
409            account.storage().to_commitment(),
410            account.storage().slots().iter(),
411        )?;
412
413        Self::insert_assets(&tx, account.vault().root(), account.vault().assets())?;
414        Self::insert_account_header(&tx, &account.into(), account.seed())?;
415
416        Self::insert_address(&tx, initial_address, account.id())?;
417
418        tx.commit().into_store_error()?;
419
420        let mut smt_forest = smt_forest.write().expect("smt_forest write lock not poisoned");
421        smt_forest.insert_and_register_account_state(
422            account.id(),
423            account.vault(),
424            account.storage(),
425        )?;
426
427        Ok(())
428    }
429
430    pub(crate) fn update_account(
431        conn: &mut Connection,
432        smt_forest: &Arc<RwLock<AccountSmtForest>>,
433        new_account_state: &Account,
434    ) -> Result<(), StoreError> {
435        const QUERY: &str = "SELECT id FROM accounts WHERE id = ?";
436        if conn
437            .prepare(QUERY)
438            .into_store_error()?
439            .query_map(params![new_account_state.id().to_hex()], |row| row.get(0))
440            .into_store_error()?
441            .map(|result| {
442                result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
443                    |id: String| {
444                        AccountId::from_hex(&id).map_err(|err| {
445                            StoreError::AccountError(
446                                AccountError::FinalAccountHeaderIdParsingFailed(err),
447                            )
448                        })
449                    },
450                )
451            })
452            .next()
453            .is_none()
454        {
455            return Err(StoreError::AccountDataNotFound(new_account_state.id()));
456        }
457
458        let mut smt_forest = smt_forest.write().expect("smt_forest write lock not poisoned");
459        let tx = conn.transaction().into_store_error()?;
460        Self::update_account_state(&tx, &mut smt_forest, new_account_state)?;
461        tx.commit().into_store_error()
462    }
463
464    pub fn upsert_foreign_account_code(
465        conn: &mut Connection,
466        account_id: AccountId,
467        code: &AccountCode,
468    ) -> Result<(), StoreError> {
469        let tx = conn.transaction().into_store_error()?;
470
471        Self::insert_account_code(&tx, code)?;
472
473        const QUERY: &str =
474            insert_sql!(foreign_account_code { account_id, code_commitment } | REPLACE);
475
476        tx.execute(QUERY, params![account_id.to_hex(), code.commitment().to_string()])
477            .into_store_error()?;
478
479        Self::insert_account_code(&tx, code)?;
480        tx.commit().into_store_error()
481    }
482
483    pub(crate) fn insert_address(
484        tx: &Transaction<'_>,
485        address: &Address,
486        account_id: AccountId,
487    ) -> Result<(), StoreError> {
488        let derived_note_tag = address.to_note_tag();
489        let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
490
491        add_note_tag_tx(tx, &note_tag_record)?;
492        Self::insert_address_internal(tx, address, account_id)?;
493
494        Ok(())
495    }
496
497    pub(crate) fn remove_address(
498        conn: &mut Connection,
499        address: &Address,
500        account_id: AccountId,
501    ) -> Result<(), StoreError> {
502        let derived_note_tag = address.to_note_tag();
503        let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
504
505        let tx = conn.transaction().into_store_error()?;
506        remove_note_tag_tx(&tx, note_tag_record)?;
507        Self::remove_address_internal(&tx, address)?;
508
509        tx.commit().into_store_error()
510    }
511
512    /// Inserts an [`AccountCode`].
513    pub(crate) fn insert_account_code(
514        tx: &Transaction<'_>,
515        account_code: &AccountCode,
516    ) -> Result<(), StoreError> {
517        const QUERY: &str = insert_sql!(account_code { commitment, code } | IGNORE);
518        tx.execute(QUERY, params![account_code.commitment().to_hex(), account_code.to_bytes()])
519            .into_store_error()?;
520        Ok(())
521    }
522
523    /// Applies the account delta to the account state, updating the vault and storage maps.
524    ///
525    /// The apply delta operation strats by copying over the initial account state (vault and
526    /// storage) and then applying the delta on top of it. The storage and vault elements are
527    /// overwritten in the new state. In the cases where the delta depends on previous state (e.g.
528    /// adding or subtracting fungible assets), the previous state needs to be provided via the
529    /// `updated_fungible_assets` and `updated_storage_maps` parameters.
530    pub(crate) fn apply_account_delta(
531        tx: &Transaction<'_>,
532        smt_forest: &mut AccountSmtForest,
533        init_account_state: &AccountHeader,
534        final_account_state: &AccountHeader,
535        updated_fungible_assets: BTreeMap<AccountIdPrefix, FungibleAsset>,
536        updated_storage_maps: BTreeMap<StorageSlotName, StorageMap>,
537        delta: &AccountDelta,
538    ) -> Result<(), StoreError> {
539        // Copy over the storage and vault from the previous state. Non-relevant data will not be
540        // modified.
541        Self::copy_account_state(tx, init_account_state, final_account_state)?;
542
543        Self::apply_account_vault_delta(
544            tx,
545            smt_forest,
546            init_account_state,
547            final_account_state,
548            updated_fungible_assets,
549            delta,
550        )?;
551
552        // Build the final roots from the init state's registered roots:
553        // - Replace vault root with the final one
554        // - Replace changed map roots with their new values (done by apply_account_storage_delta)
555        // - Unchanged map roots continue as they were
556        let mut final_roots = smt_forest
557            .get_roots(&init_account_state.id())
558            .cloned()
559            .ok_or(StoreError::AccountDataNotFound(init_account_state.id()))?;
560
561        // First element is always the vault root
562        if let Some(vault_root) = final_roots.first_mut() {
563            *vault_root = final_account_state.vault_root();
564        }
565
566        let updated_storage_slots = Self::apply_account_storage_delta(
567            smt_forest,
568            &mut final_roots,
569            updated_storage_maps,
570            delta,
571        )?;
572
573        Self::insert_storage_slots(
574            tx,
575            final_account_state.storage_commitment(),
576            updated_storage_slots.iter(),
577        )?;
578
579        smt_forest.stage_roots(final_account_state.id(), final_roots);
580
581        Ok(())
582    }
583
584    /// Removes discarded account states from the database and restores previous
585    /// roots in the SMT forest.
586    pub(crate) fn undo_account_state(
587        tx: &Transaction<'_>,
588        smt_forest: &mut AccountSmtForest,
589        discarded_states: &[(AccountId, Word)],
590    ) -> Result<(), StoreError> {
591        if discarded_states.is_empty() {
592            return Ok(());
593        }
594
595        let commitment_params = Rc::new(
596            discarded_states
597                .iter()
598                .map(|(_, commitment)| Value::from(commitment.to_hex()))
599                .collect::<Vec<_>>(),
600        );
601
602        const DELETE_QUERY: &str = "DELETE FROM accounts WHERE account_commitment IN rarray(?)";
603        tx.execute(DELETE_QUERY, params![commitment_params]).into_store_error()?;
604
605        // Discard each rolled-back state from the in-memory forest,
606        // restoring the previous roots for each account.
607        for (account_id, _) in discarded_states {
608            smt_forest.discard_roots(*account_id);
609        }
610
611        Ok(())
612    }
613
614    /// Replaces the account state with a completely new one from the network.
615    ///
616    /// Inserts all vault/storage data and atomically replaces the SMT forest roots.
617    pub(crate) fn update_account_state(
618        tx: &Transaction<'_>,
619        smt_forest: &mut AccountSmtForest,
620        new_account_state: &Account,
621    ) -> Result<(), StoreError> {
622        smt_forest.insert_and_register_account_state(
623            new_account_state.id(),
624            new_account_state.vault(),
625            new_account_state.storage(),
626        )?;
627        Self::insert_storage_slots(
628            tx,
629            new_account_state.storage().to_commitment(),
630            new_account_state.storage().slots().iter(),
631        )?;
632        Self::insert_assets(
633            tx,
634            new_account_state.vault().root(),
635            new_account_state.vault().assets(),
636        )?;
637        Self::insert_account_header(tx, &new_account_state.into(), None)?;
638
639        Ok(())
640    }
641
642    /// Locks the account if the mismatched digest doesn't belong to a previous account state (stale
643    /// data).
644    pub(crate) fn lock_account_on_unexpected_commitment(
645        tx: &Transaction<'_>,
646        account_id: &AccountId,
647        mismatched_digest: &Word,
648    ) -> Result<(), StoreError> {
649        // Mismatched digests may be due to stale network data. If the mismatched digest is
650        // tracked in the db and corresponds to the mismatched account, it means we
651        // got a past update and shouldn't lock the account.
652        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)";
653        tx.execute(
654            QUERY,
655            named_params! {
656                ":account_id": account_id.to_hex(),
657                ":digest": mismatched_digest.to_string()
658            },
659        )
660        .into_store_error()?;
661        Ok(())
662    }
663
664    // HELPERS
665    // --------------------------------------------------------------------------------------------
666
667    /// Inserts the new final account header and copies over the previous account state.
668    fn copy_account_state(
669        tx: &Transaction<'_>,
670        init_account_header: &AccountHeader,
671        final_account_header: &AccountHeader,
672    ) -> Result<(), StoreError> {
673        Self::insert_account_header(tx, final_account_header, None)?;
674
675        if init_account_header.vault_root() != final_account_header.vault_root() {
676            const VAULT_QUERY: &str = "
677                INSERT OR IGNORE INTO account_assets (
678                    root,
679                    vault_key,
680                    faucet_id_prefix,
681                    asset
682                )
683                SELECT
684                    ?, --new root
685                    vault_key,
686                    faucet_id_prefix,
687                    asset
688                FROM account_assets
689                WHERE root = (SELECT vault_root FROM accounts WHERE account_commitment = ?)
690                ";
691            tx.execute(
692                VAULT_QUERY,
693                params![
694                    final_account_header.vault_root().to_hex(),
695                    init_account_header.commitment().to_hex()
696                ],
697            )
698            .into_store_error()?;
699        }
700
701        if init_account_header.storage_commitment() != final_account_header.storage_commitment() {
702            const STORAGE_QUERY: &str = "
703                INSERT OR IGNORE INTO account_storage (
704                    commitment,
705                    slot_name,
706                    slot_value,
707                    slot_type
708                )
709                SELECT
710                    ?, -- new commitment
711                    slot_name,
712                    slot_value,
713                    slot_type
714                FROM account_storage
715                WHERE commitment = (SELECT storage_commitment FROM accounts WHERE account_commitment = ?)
716                ";
717
718            tx.execute(
719                STORAGE_QUERY,
720                params![
721                    final_account_header.storage_commitment().to_hex(),
722                    init_account_header.commitment().to_hex()
723                ],
724            )
725            .into_store_error()?;
726        }
727
728        Ok(())
729    }
730
731    /// Inserts a new account record into the database.
732    fn insert_account_header(
733        tx: &Transaction<'_>,
734        account: &AccountHeader,
735        account_seed: Option<Word>,
736    ) -> Result<(), StoreError> {
737        let id: String = account.id().to_hex();
738        let code_commitment = account.code_commitment().to_string();
739        let storage_commitment = account.storage_commitment().to_string();
740        let vault_root = account.vault_root().to_string();
741        let nonce = u64_to_value(account.nonce().as_int());
742        let commitment = account.commitment().to_string();
743
744        let account_seed = account_seed.map(|seed| seed.to_bytes());
745
746        const QUERY: &str = insert_sql!(
747            accounts {
748                id,
749                code_commitment,
750                storage_commitment,
751                vault_root,
752                nonce,
753                account_seed,
754                account_commitment,
755                locked
756            } | REPLACE
757        );
758
759        tx.execute(
760            QUERY,
761            params![
762                id,
763                code_commitment,
764                storage_commitment,
765                vault_root,
766                nonce,
767                account_seed,
768                commitment,
769                false,
770            ],
771        )
772        .into_store_error()?;
773
774        Self::insert_tracked_account_id_tx(tx, account.id())?;
775        Ok(())
776    }
777
778    fn insert_tracked_account_id_tx(
779        tx: &Transaction<'_>,
780        account_id: AccountId,
781    ) -> Result<(), StoreError> {
782        const QUERY: &str = insert_sql!(tracked_accounts { id } | IGNORE);
783        tx.execute(QUERY, params![account_id.to_hex()]).into_store_error()?;
784        Ok(())
785    }
786
787    fn insert_address_internal(
788        tx: &Transaction<'_>,
789        address: &Address,
790        account_id: AccountId,
791    ) -> Result<(), StoreError> {
792        const QUERY: &str = insert_sql!(addresses { address, account_id } | REPLACE);
793        let serialized_address = address.to_bytes();
794        tx.execute(QUERY, params![serialized_address, account_id.to_hex(),])
795            .into_store_error()?;
796
797        Ok(())
798    }
799
800    fn remove_address_internal(tx: &Transaction<'_>, address: &Address) -> Result<(), StoreError> {
801        let serialized_address = address.to_bytes();
802
803        const DELETE_QUERY: &str = "DELETE FROM addresses WHERE address = ?";
804        tx.execute(DELETE_QUERY, params![serialized_address]).into_store_error()?;
805
806        Ok(())
807    }
808}