miden_client_sqlite_store/
account.rs

1#![allow(clippy::items_after_statements)]
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    StorageMap,
19    StorageSlot,
20    StorageSlotType,
21};
22use miden_client::asset::{Asset, AssetVault, AssetWitness, FungibleAsset, NonFungibleDeltaAction};
23use miden_client::crypto::{MerkleStore, SmtLeaf, SmtProof};
24use miden_client::store::{AccountRecord, AccountStatus, StoreError};
25use miden_client::sync::NoteTagRecord;
26use miden_client::utils::{Deserializable, Serializable};
27use miden_client::{AccountError, Felt, Word};
28use miden_objects::account::StorageMapWitness;
29use miden_objects::asset::AssetVaultKey;
30use rusqlite::types::Value;
31use rusqlite::{Connection, Params, Transaction, named_params, params};
32
33use super::{SqliteStore, column_value_as_u64, u64_to_value};
34use crate::merkle_store::{
35    get_asset_proof,
36    get_storage_map_item_proof,
37    insert_asset_nodes,
38    insert_storage_map_nodes,
39    update_asset_nodes,
40    update_storage_map_nodes,
41};
42use crate::sql_error::SqlResultExt;
43use crate::sync::{add_note_tag_tx, remove_note_tag_tx};
44use crate::{insert_sql, subst};
45
46// TYPES
47// ================================================================================================
48struct SerializedHeaderData {
49    id: String,
50    nonce: u64,
51    vault_root: String,
52    storage_commitment: String,
53    code_commitment: String,
54    account_seed: Option<Vec<u8>>,
55    locked: bool,
56}
57
58impl SqliteStore {
59    // ACCOUNTS
60    // --------------------------------------------------------------------------------------------
61
62    pub(super) fn get_account_ids(conn: &mut Connection) -> Result<Vec<AccountId>, StoreError> {
63        const QUERY: &str = "SELECT id FROM tracked_accounts";
64
65        conn.prepare(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(super) 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(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    pub(crate) fn get_account(
150        conn: &mut Connection,
151        account_id: AccountId,
152    ) -> Result<Option<AccountRecord>, StoreError> {
153        let Some((header, status)) = Self::get_account_header(conn, account_id)? else {
154            return Ok(None);
155        };
156
157        let assets = query_vault_assets(conn, "root = ?", params![header.vault_root().to_hex()])?;
158        let vault = AssetVault::new(&assets)?;
159
160        let slots = query_storage_slots(
161            conn,
162            "commitment = ?",
163            params![header.storage_commitment().to_hex()],
164        )?
165        .into_values()
166        .collect();
167
168        let storage = AccountStorage::new(slots)?;
169
170        let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
171            return Ok(None);
172        };
173
174        let account = Account::new_unchecked(
175            header.id(),
176            vault,
177            storage,
178            account_code,
179            header.nonce(),
180            status.seed().copied(),
181        );
182
183        let addresses = query_account_addresses(conn, header.id())?;
184        Ok(Some(AccountRecord::new(account, status, addresses)))
185    }
186
187    pub(crate) fn insert_account(
188        conn: &mut Connection,
189        merkle_store: &Arc<RwLock<MerkleStore>>,
190        account: &Account,
191        initial_address: &Address,
192    ) -> Result<(), StoreError> {
193        let tx = conn.transaction().into_store_error()?;
194
195        Self::insert_account_code(&tx, account.code())?;
196
197        Self::insert_storage_slots(
198            &tx,
199            account.storage().commitment(),
200            account.storage().slots().iter().enumerate(),
201        )?;
202
203        Self::insert_assets(&tx, account.vault().root(), account.vault().assets())?;
204        Self::insert_account_header(&tx, &account.into(), account.seed())?;
205
206        Self::insert_address(&tx, initial_address, account.id())?;
207
208        tx.commit().into_store_error()?;
209
210        let mut merkle_store = merkle_store.write().expect("merkle_store write lock not poisoned");
211        insert_storage_map_nodes(&mut merkle_store, account.storage());
212        insert_asset_nodes(&mut merkle_store, account.vault());
213
214        Ok(())
215    }
216
217    pub(crate) fn update_account(
218        conn: &mut Connection,
219        merkle_store: &Arc<RwLock<MerkleStore>>,
220        new_account_state: &Account,
221    ) -> Result<(), StoreError> {
222        const QUERY: &str = "SELECT id FROM accounts WHERE id = ?";
223        if conn
224            .prepare(QUERY)
225            .into_store_error()?
226            .query_map(params![new_account_state.id().to_hex()], |row| row.get(0))
227            .into_store_error()?
228            .map(|result| {
229                result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
230                    |id: String| {
231                        AccountId::from_hex(&id).map_err(|err| {
232                            StoreError::AccountError(
233                                AccountError::FinalAccountHeaderIdParsingFailed(err),
234                            )
235                        })
236                    },
237                )
238            })
239            .next()
240            .is_none()
241        {
242            return Err(StoreError::AccountDataNotFound(new_account_state.id()));
243        }
244
245        let mut merkle_store = merkle_store.write().expect("merkle_store write lock not poisoned");
246        let tx = conn.transaction().into_store_error()?;
247        Self::update_account_state(&tx, &mut merkle_store, new_account_state)?;
248        tx.commit().into_store_error()
249    }
250
251    pub fn upsert_foreign_account_code(
252        conn: &mut Connection,
253        account_id: AccountId,
254        code: &AccountCode,
255    ) -> Result<(), StoreError> {
256        let tx = conn.transaction().into_store_error()?;
257
258        Self::insert_account_code(&tx, code)?;
259
260        const QUERY: &str =
261            insert_sql!(foreign_account_code { account_id, code_commitment } | REPLACE);
262
263        tx.execute(QUERY, params![account_id.to_hex(), code.commitment().to_string()])
264            .into_store_error()?;
265
266        Self::insert_account_code(&tx, code)?;
267        tx.commit().into_store_error()
268    }
269
270    pub fn get_foreign_account_code(
271        conn: &mut Connection,
272        account_ids: Vec<AccountId>,
273    ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError> {
274        let params: Vec<Value> =
275            account_ids.into_iter().map(|id| Value::from(id.to_hex())).collect();
276        const QUERY: &str = "
277            SELECT account_id, code
278            FROM foreign_account_code JOIN account_code ON foreign_account_code.code_commitment = account_code.commitment
279            WHERE account_id IN rarray(?)";
280
281        conn.prepare(QUERY)
282            .into_store_error()?
283            .query_map([Rc::new(params)], |row| Ok((row.get(0)?, row.get(1)?)))
284            .expect("no binding parameters used in query")
285            .map(|result| {
286                result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
287                    |(id, code): (String, Vec<u8>)| {
288                        Ok((
289                            AccountId::from_hex(&id).map_err(|err| {
290                                StoreError::AccountError(
291                                    AccountError::FinalAccountHeaderIdParsingFailed(err),
292                                )
293                            })?,
294                            AccountCode::from_bytes(&code).map_err(StoreError::AccountError)?,
295                        ))
296                    },
297                )
298            })
299            .collect::<Result<BTreeMap<AccountId, AccountCode>, _>>()
300    }
301
302    /// Retrieves the full asset vault for a specific account.
303    pub fn get_account_vault(
304        conn: &Connection,
305        account_id: AccountId,
306    ) -> Result<AssetVault, StoreError> {
307        let assets = query_vault_assets(
308            conn,
309            "root = (SELECT vault_root FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1)",
310            params![account_id.to_hex()],
311        )?;
312
313        Ok(AssetVault::new(&assets)?)
314    }
315
316    /// Retrieves the full storage for a specific account.
317    pub fn get_account_storage(
318        conn: &Connection,
319        account_id: AccountId,
320    ) -> Result<AccountStorage, StoreError> {
321        let slots = query_storage_slots(
322            conn,
323            "commitment = (SELECT storage_commitment FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1)",
324            params![account_id.to_hex()],
325        )?
326        .into_values()
327        .collect();
328
329        Ok(AccountStorage::new(slots)?)
330    }
331
332    /// Fetches a specific asset from the account's vault without the need of loading the entire
333    /// vault. The Merkle proof is also retrieved from the [`MerkleStore`].
334    pub(crate) fn get_account_asset(
335        conn: &mut Connection,
336        merkle_store: &Arc<RwLock<MerkleStore>>,
337        account_id: AccountId,
338        faucet_id_prefix: AccountIdPrefix,
339    ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
340        let header = Self::get_account_header(conn, account_id)?
341            .ok_or(StoreError::AccountDataNotFound(account_id))?
342            .0;
343
344        let Some(asset) = query_vault_assets(
345            conn,
346            "faucet_id_prefix = ? AND root = ?",
347            params![faucet_id_prefix.to_hex(), header.vault_root().to_hex()],
348        )?
349        .into_iter()
350        .next() else {
351            return Ok(None);
352        };
353
354        let merkle_store = merkle_store.read().expect("merkle_store read lock not poisoned");
355
356        let proof = get_asset_proof(&merkle_store, header.vault_root(), &asset)?;
357        let witness = AssetWitness::new(proof)?;
358
359        Ok(Some((asset, witness)))
360    }
361
362    /// Retrieves a specific item from the account's storage map without loading the entire storage.
363    /// The Merkle proof is also retrieved from the [`MerkleStore`].
364    pub(crate) fn get_account_map_item(
365        conn: &mut Connection,
366        merkle_store: &Arc<RwLock<MerkleStore>>,
367        account_id: AccountId,
368        index: u8,
369        key: Word,
370    ) -> Result<(Word, StorageMapWitness), StoreError> {
371        let header = Self::get_account_header(conn, account_id)?
372            .ok_or(StoreError::AccountDataNotFound(account_id))?
373            .0;
374
375        let StorageSlot::Map(map) = query_storage_slots(
376            conn,
377            "commitment = ? AND slot_index = ?",
378            params![header.storage_commitment().to_hex(), index],
379        )?
380        .remove(&index)
381        .ok_or(StoreError::AccountStorageNotFound(header.storage_commitment()))?
382        else {
383            return Err(StoreError::AccountError(AccountError::StorageSlotNotMap(index)));
384        };
385
386        let item = map.get(&key);
387        let merkle_store = merkle_store.read().expect("merkle_store read lock not poisoned");
388
389        // TODO: change the api of get_storage_map_item_proof
390        let path = get_storage_map_item_proof(&merkle_store, map.root(), key)?.1.try_into()?;
391        let leaf = SmtLeaf::new_single(StorageMap::hash_key(key), item);
392        let proof = SmtProof::new(path, leaf)?;
393
394        let witness = StorageMapWitness::new(proof, [key])?;
395
396        Ok((item, witness))
397    }
398
399    pub(crate) fn get_account_addresses(
400        conn: &mut Connection,
401        account_id: AccountId,
402    ) -> Result<Vec<Address>, StoreError> {
403        query_account_addresses(conn, account_id)
404    }
405
406    pub(crate) fn insert_address(
407        tx: &Transaction<'_>,
408        address: &Address,
409        account_id: AccountId,
410    ) -> Result<(), StoreError> {
411        let derived_note_tag = address.to_note_tag();
412        let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
413
414        add_note_tag_tx(tx, &note_tag_record)?;
415        Self::insert_address_internal(tx, address, account_id)?;
416
417        Ok(())
418    }
419
420    pub(crate) fn remove_address(
421        conn: &mut Connection,
422        address: &Address,
423        account_id: AccountId,
424    ) -> Result<(), StoreError> {
425        let derived_note_tag = address.to_note_tag();
426        let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
427
428        let tx = conn.transaction().into_store_error()?;
429        remove_note_tag_tx(&tx, note_tag_record)?;
430        Self::remove_address_internal(&tx, address)?;
431
432        tx.commit().into_store_error()
433    }
434
435    // ACCOUNT DELTA HELPERS
436    // --------------------------------------------------------------------------------------------
437
438    /// Applies the account delta to the account state, updating the vault and storage maps.
439    ///
440    /// The apply delta operation strats by copying over the initial account state (vault and
441    /// storage) and then applying the delta on top of it. The storage and vault elements are
442    /// overwritten in the new state. In the cases where the delta depends on previous state (e.g.
443    /// adding or subtracting fungible assets), the previous state needs to be provided via the
444    /// `updated_fungible_assets` and `updated_storage_maps` parameters.
445    pub(super) fn apply_account_delta(
446        tx: &Transaction<'_>,
447        merkle_store: &mut MerkleStore,
448        init_account_state: &AccountHeader,
449        final_account_state: &AccountHeader,
450        mut updated_fungible_assets: BTreeMap<AccountIdPrefix, FungibleAsset>,
451        mut updated_storage_maps: BTreeMap<u8, StorageMap>,
452        delta: &AccountDelta,
453    ) -> Result<(), StoreError> {
454        // Copy over the storage and vault from the previous state. Non-relevant data will not be
455        // modified.
456        Self::copy_account_state(tx, init_account_state, final_account_state)?;
457
458        // Apply vault delta. This map will contain all updated assets (indexed by vault key), both
459        // fungible and non-fungible.
460        let mut updated_assets: BTreeMap<AssetVaultKey, Asset> = BTreeMap::new();
461        let mut removed_vault_keys: Vec<AssetVaultKey> = Vec::new();
462
463        // We first process the fungible assets. Adding or subtracting them from the vault as
464        // requested.
465        for (faucet_id, delta) in delta.vault().fungible().iter() {
466            let delta_asset = FungibleAsset::new(*faucet_id, delta.unsigned_abs())?;
467
468            let asset = match updated_fungible_assets.remove(&faucet_id.prefix()) {
469                Some(asset) => {
470                    // If the asset exists, update it accordingly.
471                    if *delta >= 0 {
472                        asset.add(delta_asset)?
473                    } else {
474                        asset.sub(delta_asset)?
475                    }
476                },
477                None => {
478                    // If the asset doesn't exist, we add it to the map to be inserted.
479                    delta_asset
480                },
481            };
482
483            if asset.amount() > 0 {
484                updated_assets.insert(asset.vault_key(), Asset::Fungible(asset));
485            } else {
486                removed_vault_keys.push(asset.vault_key());
487            }
488        }
489
490        // Process non-fungible assets. Here additions or removals don't depend on previous state as
491        // each asset is unique.
492        let (added_nonfungible_assets, removed_nonfungible_assets) =
493            delta.vault().non_fungible().iter().partition::<Vec<_>, _>(|(_, action)| {
494                matches!(action, NonFungibleDeltaAction::Add)
495            });
496
497        updated_assets.extend(
498            added_nonfungible_assets
499                .into_iter()
500                .map(|(asset, _)| (asset.vault_key(), Asset::NonFungible(*asset))),
501        );
502
503        removed_vault_keys
504            .extend(removed_nonfungible_assets.iter().map(|(asset, _)| asset.vault_key()));
505
506        const DELETE_QUERY: &str =
507            "DELETE FROM account_assets WHERE root = ? AND vault_key IN rarray(?)";
508
509        tx.execute(
510            DELETE_QUERY,
511            params![
512                final_account_state.vault_root().to_hex(),
513                Rc::new(
514                    removed_vault_keys
515                        .into_iter()
516                        .map(|k| {
517                            let k_word: Word = k.into();
518                            Value::from(k_word.to_hex())
519                        })
520                        .collect::<Vec<Value>>(),
521                ),
522            ],
523        )
524        .into_store_error()?;
525
526        update_asset_nodes(
527            merkle_store,
528            init_account_state.vault_root(),
529            updated_assets.values().copied(),
530        )?;
531        Self::insert_assets(tx, final_account_state.vault_root(), updated_assets.into_values())?;
532
533        // Apply storage delta. This map will contain all updated storage slots, both values and
534        // maps. It gets initialized with value type updates which contain the new value and
535        // don't depend on previous state.
536        let mut updated_storage_slots: BTreeMap<u8, StorageSlot> = delta
537            .storage()
538            .values()
539            .iter()
540            .map(|(index, slot)| (*index, StorageSlot::Value(*slot)))
541            .collect();
542
543        // For storage map deltas, we only updated the keys in the delta, this is why we need the
544        // previously retrieved storage maps.
545        for (index, map_delta) in delta.storage().maps() {
546            let mut map = updated_storage_maps.remove(index).unwrap_or_default();
547
548            update_storage_map_nodes(
549                merkle_store,
550                map.root(),
551                map_delta.entries().iter().map(|(key, value)| ((*key).into(), *value)),
552            )?;
553
554            for (key, value) in map_delta.entries() {
555                map.insert((*key).into(), *value)?;
556            }
557
558            updated_storage_slots.insert(*index, StorageSlot::Map(map));
559        }
560
561        Self::insert_storage_slots(
562            tx,
563            final_account_state.storage_commitment(),
564            updated_storage_slots.iter().map(|(index, slot)| (*index as usize, slot)),
565        )?;
566
567        Ok(())
568    }
569
570    /// Fetches the relevant fungible assets of an account that will be updated by the account
571    /// delta.
572    pub(super) fn get_account_fungible_assets_for_delta(
573        conn: &Connection,
574        header: &AccountHeader,
575        delta: &AccountDelta,
576    ) -> Result<BTreeMap<AccountIdPrefix, FungibleAsset>, StoreError> {
577        let fungible_faucet_prefixes = delta
578            .vault()
579            .fungible()
580            .iter()
581            .map(|(faucet_id, _)| Value::Text(faucet_id.prefix().to_hex()))
582            .collect::<Vec<Value>>();
583
584        Ok(query_vault_assets(
585            conn,
586            "root = ? AND faucet_id_prefix IN rarray(?)",
587            params![header.vault_root().to_hex(), Rc::new(fungible_faucet_prefixes)]
588                )?
589                .into_iter()
590                // SAFETY: all retrieved assets should be fungible
591                .map(|asset| (asset.faucet_id_prefix(), asset.unwrap_fungible()))
592                .collect())
593    }
594
595    /// Fetches the relevant storage maps inside the account's storage that will be updated by the
596    /// account delta.
597    pub(super) fn get_account_storage_maps_for_delta(
598        conn: &Connection,
599        header: &AccountHeader,
600        delta: &AccountDelta,
601    ) -> Result<BTreeMap<u8, StorageMap>, StoreError> {
602        let updated_map_indexes = delta
603            .storage()
604            .maps()
605            .keys()
606            .map(|k| Value::Integer(i64::from(*k)))
607            .collect::<Vec<Value>>();
608
609        query_storage_slots(
610            conn,
611            "commitment = ? AND slot_index IN rarray(?)",
612            params![header.storage_commitment().to_hex(), Rc::new(updated_map_indexes)],
613        )?
614        .into_iter()
615        .map(|(index, slot)| {
616            let StorageSlot::Map(map) = slot else {
617                return Err(StoreError::AccountError(AccountError::StorageSlotNotMap(index)));
618            };
619
620            Ok((index, map))
621        })
622        .collect()
623    }
624
625    /// Inserts the new `final_account_header` to the store and copies over the previous account
626    /// state (vault and storage). This isn't meant to be the whole account update, just the first
627    /// step. The account delta should then be applied to the copied data.
628    fn copy_account_state(
629        tx: &Transaction<'_>,
630        init_account_header: &AccountHeader,
631        final_account_header: &AccountHeader,
632    ) -> Result<(), StoreError> {
633        Self::insert_account_header(tx, final_account_header, None)?;
634
635        if init_account_header.vault_root() != final_account_header.vault_root() {
636            const VAULT_QUERY: &str = "
637                INSERT OR IGNORE INTO account_assets (
638                    root,
639                    vault_key,
640                    faucet_id_prefix,
641                    asset
642                )
643                SELECT
644                    ?, --new root
645                    vault_key,
646                    faucet_id_prefix,
647                    asset
648                FROM account_assets
649                WHERE root = (SELECT vault_root FROM accounts WHERE account_commitment = ?)
650                ";
651            tx.execute(
652                VAULT_QUERY,
653                params![
654                    final_account_header.vault_root().to_hex(),
655                    init_account_header.commitment().to_hex()
656                ],
657            )
658            .into_store_error()?;
659        }
660
661        if init_account_header.storage_commitment() != final_account_header.storage_commitment() {
662            const STORAGE_QUERY: &str = "
663                INSERT OR IGNORE INTO account_storage (
664                    commitment,
665                    slot_index,
666                    slot_value,
667                    slot_type
668                )
669                SELECT
670                    ?, -- new commitment
671                    slot_index,
672                    slot_value,
673                    slot_type
674                FROM account_storage
675                WHERE commitment = (SELECT storage_commitment FROM accounts WHERE account_commitment = ?)
676                ";
677
678            tx.execute(
679                STORAGE_QUERY,
680                params![
681                    final_account_header.storage_commitment().to_hex(),
682                    init_account_header.commitment().to_hex()
683                ],
684            )
685            .into_store_error()?;
686        }
687
688        Ok(())
689    }
690
691    // HELPERS
692    // --------------------------------------------------------------------------------------------
693
694    /// Update previously-existing account after a transaction execution. Apart from updating the
695    /// `SQLite` database, this function also updates the [`MerkleStore`] by adding the vault and
696    /// storage SMT's nodes.
697    ///
698    /// Because the Client retrieves the account by account ID before applying the delta, we don't
699    /// need to check that it exists here. This inserts a new row into the accounts table.
700    /// We can later identify the proper account state by looking at the nonce.
701    pub(super) fn update_account_state(
702        tx: &Transaction<'_>,
703        merkle_store: &mut MerkleStore,
704        new_account_state: &Account,
705    ) -> Result<(), StoreError> {
706        insert_storage_map_nodes(merkle_store, new_account_state.storage());
707        Self::insert_storage_slots(
708            tx,
709            new_account_state.storage().commitment(),
710            new_account_state.storage().slots().iter().enumerate(),
711        )?;
712        insert_asset_nodes(merkle_store, new_account_state.vault());
713        Self::insert_assets(
714            tx,
715            new_account_state.vault().root(),
716            new_account_state.vault().assets(),
717        )?;
718        Self::insert_account_header(tx, &new_account_state.into(), None)
719    }
720
721    /// Locks the account if the mismatched digest doesn't belong to a previous account state (stale
722    /// data).
723    pub(super) fn lock_account_on_unexpected_commitment(
724        tx: &Transaction<'_>,
725        account_id: &AccountId,
726        mismatched_digest: &Word,
727    ) -> Result<(), StoreError> {
728        // Mismatched digests may be due to stale network data. If the mismatched digest is
729        // tracked in the db and corresponds to the mismatched account, it means we
730        // got a past update and shouldn't lock the account.
731        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)";
732        tx.execute(
733            QUERY,
734            named_params! {
735                ":account_id": account_id.to_hex(),
736                ":digest": mismatched_digest.to_string()
737            },
738        )
739        .into_store_error()?;
740        Ok(())
741    }
742
743    /// Removes account states with the specified hashes from the database.
744    ///
745    /// This is used to rollback account changes when a transaction is discarded,
746    /// effectively undoing the account state changes that were applied by the transaction.
747    ///
748    /// Note: This is not part of the Store trait and is only used internally by the `SQLite` store
749    /// implementation to handle transaction rollbacks.
750    pub(super) fn undo_account_state(
751        tx: &Transaction<'_>,
752        account_hashes: &[Word],
753    ) -> Result<(), StoreError> {
754        const QUERY: &str = "DELETE FROM accounts WHERE account_commitment IN rarray(?)";
755
756        let params = account_hashes.iter().map(|h| Value::from(h.to_hex())).collect::<Vec<_>>();
757        tx.execute(QUERY, params![Rc::new(params)]).into_store_error()?;
758
759        Ok(())
760    }
761
762    /// Inserts a new account record into the database.
763    fn insert_account_header(
764        tx: &Transaction<'_>,
765        account: &AccountHeader,
766        account_seed: Option<Word>,
767    ) -> Result<(), StoreError> {
768        let id: String = account.id().to_hex();
769        let code_commitment = account.code_commitment().to_string();
770        let storage_commitment = account.storage_commitment().to_string();
771        let vault_root = account.vault_root().to_string();
772        let nonce = u64_to_value(account.nonce().as_int());
773        let commitment = account.commitment().to_string();
774
775        let account_seed = account_seed.map(|seed| seed.to_bytes());
776
777        const QUERY: &str = insert_sql!(
778            accounts {
779                id,
780                code_commitment,
781                storage_commitment,
782                vault_root,
783                nonce,
784                account_seed,
785                account_commitment,
786                locked
787            } | REPLACE
788        );
789
790        tx.execute(
791            QUERY,
792            params![
793                id,
794                code_commitment,
795                storage_commitment,
796                vault_root,
797                nonce,
798                account_seed,
799                commitment,
800                false,
801            ],
802        )
803        .into_store_error()?;
804
805        Self::insert_tracked_account_id_tx(tx, account.id())?;
806        Ok(())
807    }
808
809    /// Inserts an [`AccountCode`].
810    fn insert_account_code(
811        tx: &Transaction<'_>,
812        account_code: &AccountCode,
813    ) -> Result<(), StoreError> {
814        const QUERY: &str = insert_sql!(account_code { commitment, code } | IGNORE);
815        tx.execute(QUERY, params![account_code.commitment().to_hex(), account_code.to_bytes()])
816            .into_store_error()?;
817        Ok(())
818    }
819
820    fn insert_tracked_account_id_tx(
821        tx: &Transaction<'_>,
822        account_id: AccountId,
823    ) -> Result<(), StoreError> {
824        const QUERY: &str = insert_sql!(tracked_accounts { id } | IGNORE);
825        tx.execute(QUERY, params![account_id.to_hex()]).into_store_error()?;
826        Ok(())
827    }
828
829    fn insert_storage_slots<'a>(
830        tx: &Transaction<'_>,
831        commitment: Word,
832        account_storage: impl Iterator<Item = (usize, &'a StorageSlot)>,
833    ) -> Result<(), StoreError> {
834        for (index, slot) in account_storage {
835            const QUERY: &str = insert_sql!(
836                account_storage {
837                    commitment,
838                    slot_index,
839                    slot_value,
840                    slot_type
841                } | REPLACE
842            );
843
844            tx.execute(
845                QUERY,
846                params![
847                    commitment.to_hex(),
848                    index,
849                    slot.value().to_hex(),
850                    slot.slot_type().to_bytes()
851                ],
852            )
853            .into_store_error()?;
854
855            if let StorageSlot::Map(map) = slot {
856                const MAP_QUERY: &str =
857                    insert_sql!(storage_map_entries { root, key, value } | REPLACE);
858                for (key, value) in map.entries() {
859                    // Insert each entry of the storage map
860                    tx.execute(
861                        MAP_QUERY,
862                        params![map.root().to_hex(), key.to_hex(), value.to_hex()],
863                    )
864                    .into_store_error()?;
865                }
866            }
867        }
868
869        Ok(())
870    }
871
872    fn insert_assets(
873        tx: &Transaction<'_>,
874        root: Word,
875        assets: impl Iterator<Item = Asset>,
876    ) -> Result<(), StoreError> {
877        for asset in assets {
878            let vault_key_word: Word = asset.vault_key().into();
879            const QUERY: &str =
880                insert_sql!(account_assets { root, vault_key, faucet_id_prefix, asset } | REPLACE);
881            tx.execute(
882                QUERY,
883                params![
884                    root.to_hex(),
885                    vault_key_word.to_hex(),
886                    asset.faucet_id_prefix().to_hex(),
887                    Word::from(asset).to_hex(),
888                ],
889            )
890            .into_store_error()?;
891        }
892
893        Ok(())
894    }
895
896    fn insert_address_internal(
897        tx: &Transaction<'_>,
898        address: &Address,
899        account_id: AccountId,
900    ) -> Result<(), StoreError> {
901        const QUERY: &str = insert_sql!(addresses { address, account_id } | REPLACE);
902        let serialized_address = address.to_bytes();
903        tx.execute(QUERY, params![serialized_address, account_id.to_hex(),])
904            .into_store_error()?;
905
906        Ok(())
907    }
908
909    fn remove_address_internal(tx: &Transaction<'_>, address: &Address) -> Result<(), StoreError> {
910        let serialized_address = address.to_bytes();
911
912        const DELETE_QUERY: &str = "DELETE FROM addresses WHERE address = ?";
913        tx.execute(DELETE_QUERY, params![serialized_address]).into_store_error()?;
914
915        Ok(())
916    }
917}
918
919// HELPERS
920// ================================================================================================
921
922/// Parse an account header from the provided serialized data.
923fn parse_accounts(
924    serialized_account_parts: SerializedHeaderData,
925) -> Result<(AccountHeader, AccountStatus), StoreError> {
926    let SerializedHeaderData {
927        id,
928        nonce,
929        vault_root,
930        storage_commitment,
931        code_commitment,
932        account_seed,
933        locked,
934    } = serialized_account_parts;
935    let account_seed = account_seed.map(|seed| Word::read_from_bytes(&seed)).transpose()?;
936
937    let status = match (account_seed, locked) {
938        (_, true) => AccountStatus::Locked,
939        (Some(seed), _) => AccountStatus::New { seed },
940        _ => AccountStatus::Tracked,
941    };
942
943    Ok((
944        AccountHeader::new(
945            AccountId::from_hex(&id).expect("Conversion from stored AccountID should not panic"),
946            Felt::new(nonce),
947            Word::try_from(&vault_root)?,
948            Word::try_from(&storage_commitment)?,
949            Word::try_from(&code_commitment)?,
950        ),
951        status,
952    ))
953}
954
955fn query_storage_slots(
956    conn: &Connection,
957    where_clause: &str,
958    params: impl Params,
959) -> Result<BTreeMap<u8, StorageSlot>, StoreError> {
960    const STORAGE_QUERY: &str = "SELECT slot_index, slot_value, slot_type FROM account_storage";
961
962    let query = format!("{STORAGE_QUERY} WHERE {where_clause}");
963    let storage_values = conn
964        .prepare(&query)
965        .into_store_error()?
966        .query_map(params, |row| {
967            let index: u8 = row.get(0)?;
968            let value: String = row.get(1)?;
969            let slot_type: Vec<u8> = row.get(2)?;
970            Ok((index, value, slot_type))
971        })
972        .into_store_error()?
973        .map(|result| {
974            let (index, value, slot_type) = result.into_store_error()?;
975            Ok((index, Word::try_from(value)?, StorageSlotType::read_from_bytes(&slot_type)?))
976        })
977        .collect::<Result<Vec<(u8, Word, StorageSlotType)>, StoreError>>()?;
978
979    let possible_roots: Vec<Value> =
980        storage_values.iter().map(|(_, value, _)| Value::from(value.to_hex())).collect();
981
982    let mut storage_maps =
983        query_storage_maps(conn, "root IN rarray(?)", [Rc::new(possible_roots)])?;
984
985    Ok(storage_values
986        .into_iter()
987        .map(|(index, value, slot_type)| {
988            let slot = match slot_type {
989                StorageSlotType::Value => StorageSlot::Value(value),
990                StorageSlotType::Map => {
991                    StorageSlot::Map(storage_maps.remove(&value).unwrap_or_default())
992                },
993            };
994            (index, slot)
995        })
996        .collect())
997}
998
999fn query_storage_maps(
1000    conn: &Connection,
1001    where_clause: &str,
1002    params: impl Params,
1003) -> Result<BTreeMap<Word, StorageMap>, StoreError> {
1004    const STORAGE_MAP_SELECT: &str = "SELECT root, key, value FROM storage_map_entries";
1005    let query = format!("{STORAGE_MAP_SELECT} WHERE {where_clause}");
1006
1007    let map_entries = conn
1008        .prepare(&query)
1009        .into_store_error()?
1010        .query_map(params, |row| {
1011            let root: String = row.get(0)?;
1012            let key: String = row.get(1)?;
1013            let value: String = row.get(2)?;
1014
1015            Ok((root, key, value))
1016        })
1017        .into_store_error()?
1018        .map(|result| {
1019            let (root, key, value) = result.into_store_error()?;
1020            Ok((Word::try_from(root)?, Word::try_from(key)?, Word::try_from(value)?))
1021        })
1022        .collect::<Result<Vec<(Word, Word, Word)>, StoreError>>()?;
1023
1024    let mut maps = BTreeMap::new();
1025    for (root, key, value) in map_entries {
1026        let map = maps.entry(root).or_insert_with(StorageMap::new);
1027        map.insert(key, value)?;
1028    }
1029
1030    Ok(maps)
1031}
1032
1033fn query_vault_assets(
1034    conn: &Connection,
1035    where_clause: &str,
1036    params: impl Params,
1037) -> Result<Vec<Asset>, StoreError> {
1038    const VAULT_QUERY: &str = "SELECT asset FROM account_assets";
1039
1040    let query = format!("{VAULT_QUERY} WHERE {where_clause}");
1041    conn.prepare(&query)
1042        .into_store_error()?
1043        .query_map(params, |row| {
1044            let asset: String = row.get(0)?;
1045            Ok(asset)
1046        })
1047        .into_store_error()?
1048        .map(|result| {
1049            let asset_str: String = result.into_store_error()?;
1050            let word = Word::try_from(asset_str)?;
1051            Ok(Asset::try_from(word)?)
1052        })
1053        .collect::<Result<Vec<Asset>, StoreError>>()
1054}
1055
1056fn query_account_code(
1057    conn: &Connection,
1058    commitment: Word,
1059) -> Result<Option<AccountCode>, StoreError> {
1060    // TODO: this function will probably be refactored to receive more complex where clauses and
1061    // return multiple mast forests
1062    const CODE_QUERY: &str = "SELECT code FROM account_code WHERE commitment = ?";
1063
1064    conn.prepare(CODE_QUERY)
1065        .into_store_error()?
1066        .query_map(params![commitment.to_hex()], |row| {
1067            let code: Vec<u8> = row.get(0)?;
1068            Ok(code)
1069        })
1070        .into_store_error()?
1071        .map(|result| {
1072            let bytes: Vec<u8> = result.into_store_error()?;
1073            Ok(AccountCode::from_bytes(&bytes)?)
1074        })
1075        .next()
1076        .transpose()
1077}
1078
1079fn query_account_headers(
1080    conn: &Connection,
1081    where_clause: &str,
1082    params: impl Params,
1083) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError> {
1084    const SELECT_QUERY: &str = "SELECT id, nonce, vault_root, storage_commitment, code_commitment, account_seed, locked \
1085        FROM accounts";
1086    let query = format!("{SELECT_QUERY} WHERE {where_clause}");
1087    conn.prepare(&query)
1088        .into_store_error()?
1089        .query_map(params, |row| {
1090            let id: String = row.get(0)?;
1091            let nonce: u64 = column_value_as_u64(row, 1)?;
1092            let vault_root: String = row.get(2)?;
1093            let storage_commitment: String = row.get(3)?;
1094            let code_commitment: String = row.get(4)?;
1095            let account_seed: Option<Vec<u8>> = row.get(5)?;
1096            let locked: bool = row.get(6)?;
1097
1098            Ok(SerializedHeaderData {
1099                id,
1100                nonce,
1101                vault_root,
1102                storage_commitment,
1103                code_commitment,
1104                account_seed,
1105                locked,
1106            })
1107        })
1108        .into_store_error()?
1109        .map(|result| parse_accounts(result.into_store_error()?))
1110        .collect::<Result<Vec<(AccountHeader, AccountStatus)>, StoreError>>()
1111}
1112
1113fn query_account_addresses(
1114    conn: &Connection,
1115    account_id: AccountId,
1116) -> Result<Vec<Address>, StoreError> {
1117    const ADDRESS_QUERY: &str = "SELECT address FROM addresses";
1118
1119    let query = format!("{ADDRESS_QUERY} WHERE ACCOUNT_ID = '{}'", account_id.to_hex());
1120    conn.prepare(&query)
1121        .into_store_error()?
1122        .query_map([], |row| {
1123            let address: Vec<u8> = row.get(0)?;
1124            Ok(address)
1125        })
1126        .into_store_error()?
1127        .map(|result| {
1128            let serialized_address = result.into_store_error()?;
1129            let address = Address::read_from_bytes(&serialized_address)?;
1130            Ok(address)
1131        })
1132        .collect::<Result<Vec<Address>, StoreError>>()
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137    use std::collections::BTreeMap;
1138    use std::vec::Vec;
1139
1140    use anyhow::Context;
1141    use miden_client::account::component::AccountComponent;
1142    use miden_client::account::{
1143        Account,
1144        AccountBuilder,
1145        AccountCode,
1146        AccountDelta,
1147        AccountHeader,
1148        AccountId,
1149        AccountType,
1150        Address,
1151        StorageMap,
1152        StorageSlot,
1153    };
1154    use miden_client::asset::{
1155        AccountStorageDelta,
1156        AccountVaultDelta,
1157        Asset,
1158        FungibleAsset,
1159        NonFungibleAsset,
1160        NonFungibleAssetDetails,
1161    };
1162    use miden_client::store::Store;
1163    use miden_client::testing::account_id::{
1164        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1165        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
1166    };
1167    use miden_client::testing::constants::NON_FUNGIBLE_ASSET_DATA;
1168    use miden_client::transaction::TransactionKernel;
1169    use miden_client::{EMPTY_WORD, ONE, ZERO};
1170    use miden_lib::account::auth::AuthRpoFalcon512;
1171    use miden_lib::account::components::basic_wallet_library;
1172    use miden_objects::account::auth::PublicKeyCommitment;
1173
1174    use crate::SqliteStore;
1175    use crate::sql_error::SqlResultExt;
1176    use crate::tests::create_test_store;
1177
1178    #[tokio::test]
1179    async fn account_code_insertion_no_duplicates() -> anyhow::Result<()> {
1180        let store = create_test_store().await;
1181        let assembler = TransactionKernel::assembler();
1182        let account_component = AccountComponent::compile(
1183            "
1184                export.::miden::contracts::wallets::basic::receive_asset
1185                export.::miden::contracts::wallets::basic::move_asset_to_note
1186            ",
1187            assembler,
1188            vec![],
1189        )?
1190        .with_supports_all_types();
1191        let account_code = AccountCode::from_components(
1192            &[
1193                AuthRpoFalcon512::new(PublicKeyCommitment::from(EMPTY_WORD)).into(),
1194                account_component,
1195            ],
1196            AccountType::RegularAccountUpdatableCode,
1197        )?;
1198
1199        store
1200            .interact_with_connection(move |conn| {
1201                let tx = conn.transaction().into_store_error()?;
1202
1203                // Table is empty at the beginning
1204                let mut actual: usize = tx
1205                    .query_row("SELECT Count(*) FROM account_code", [], |row| row.get(0))
1206                    .into_store_error()?;
1207                assert_eq!(actual, 0);
1208
1209                // First insertion generates a new row
1210                SqliteStore::insert_account_code(&tx, &account_code)?;
1211                actual = tx
1212                    .query_row("SELECT Count(*) FROM account_code", [], |row| row.get(0))
1213                    .into_store_error()?;
1214                assert_eq!(actual, 1);
1215
1216                // Second insertion passes but does not generate a new row
1217                assert!(SqliteStore::insert_account_code(&tx, &account_code).is_ok());
1218                actual = tx
1219                    .query_row("SELECT Count(*) FROM account_code", [], |row| row.get(0))
1220                    .into_store_error()?;
1221                assert_eq!(actual, 1);
1222
1223                Ok(())
1224            })
1225            .await?;
1226
1227        Ok(())
1228    }
1229
1230    #[tokio::test]
1231    async fn apply_account_delta_additions() -> anyhow::Result<()> {
1232        let store = create_test_store().await;
1233
1234        let dummy_component = AccountComponent::new(
1235            basic_wallet_library(),
1236            vec![StorageSlot::empty_value(), StorageSlot::empty_map()],
1237        )?
1238        .with_supports_all_types();
1239
1240        // Create and insert an account
1241        let account = AccountBuilder::new([0; 32])
1242            .account_type(AccountType::RegularAccountImmutableCode)
1243            .with_auth_component(AuthRpoFalcon512::new(PublicKeyCommitment::from(EMPTY_WORD)))
1244            .with_component(dummy_component)
1245            .build()?;
1246
1247        let default_address = Address::new(account.id());
1248        store.insert_account(&account, default_address).await?;
1249
1250        let mut storage_delta = AccountStorageDelta::new();
1251        storage_delta.set_item(1, [ZERO, ZERO, ZERO, ONE].into());
1252        storage_delta.set_map_item(2, [ONE, ZERO, ZERO, ZERO].into(), [ONE, ONE, ONE, ONE].into());
1253
1254        let vault_delta = AccountVaultDelta::from_iters(
1255            vec![
1256                FungibleAsset::new(AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?, 100)?
1257                    .into(),
1258                NonFungibleAsset::new(&NonFungibleAssetDetails::new(
1259                    AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?.prefix(),
1260                    NON_FUNGIBLE_ASSET_DATA.into(),
1261                )?)?
1262                .into(),
1263            ],
1264            [],
1265        );
1266
1267        let delta = AccountDelta::new(account.id(), storage_delta, vault_delta, ONE)?;
1268
1269        let mut account_after_delta = account.clone();
1270        account_after_delta.apply_delta(&delta)?;
1271
1272        let account_id = account.id();
1273        let final_state: AccountHeader = (&account_after_delta).into();
1274        let merkle_store = store.merkle_store.clone();
1275        store
1276            .interact_with_connection(move |conn| {
1277                let tx = conn.transaction().into_store_error()?;
1278                let mut merkle_store =
1279                    merkle_store.write().expect("merkle_store write lock not poisoned");
1280
1281                SqliteStore::apply_account_delta(
1282                    &tx,
1283                    &mut merkle_store,
1284                    &account.into(),
1285                    &final_state,
1286                    BTreeMap::default(),
1287                    BTreeMap::default(),
1288                    &delta,
1289                )?;
1290
1291                tx.commit().into_store_error()?;
1292                Ok(())
1293            })
1294            .await?;
1295
1296        let updated_account: Account = store
1297            .get_account(account_id)
1298            .await?
1299            .context("failed to find inserted account")?
1300            .into();
1301
1302        assert_eq!(updated_account, account_after_delta);
1303
1304        Ok(())
1305    }
1306
1307    #[tokio::test]
1308    async fn apply_account_delta_removals() -> anyhow::Result<()> {
1309        let store = create_test_store().await;
1310
1311        let mut dummy_map = StorageMap::new();
1312        dummy_map.insert([ONE, ZERO, ZERO, ZERO].into(), [ONE, ONE, ONE, ONE].into())?;
1313
1314        let dummy_component = AccountComponent::new(
1315            basic_wallet_library(),
1316            vec![StorageSlot::Value([ZERO, ZERO, ZERO, ONE].into()), StorageSlot::Map(dummy_map)],
1317        )?
1318        .with_supports_all_types();
1319
1320        // Create and insert an account
1321        let assets: Vec<Asset> = vec![
1322            FungibleAsset::new(AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?, 100)?
1323                .into(),
1324            NonFungibleAsset::new(&NonFungibleAssetDetails::new(
1325                AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?.prefix(),
1326                NON_FUNGIBLE_ASSET_DATA.into(),
1327            )?)?
1328            .into(),
1329        ];
1330        let account = AccountBuilder::new([0; 32])
1331            .account_type(AccountType::RegularAccountImmutableCode)
1332            .with_auth_component(AuthRpoFalcon512::new(PublicKeyCommitment::from(EMPTY_WORD)))
1333            .with_component(dummy_component)
1334            .with_assets(assets.clone())
1335            .build_existing()?;
1336        let default_address = Address::new(account.id());
1337        store.insert_account(&account, default_address).await?;
1338
1339        let mut storage_delta = AccountStorageDelta::new();
1340        storage_delta.set_item(1, EMPTY_WORD);
1341        storage_delta.set_map_item(2, [ONE, ZERO, ZERO, ZERO].into(), EMPTY_WORD);
1342
1343        let vault_delta = AccountVaultDelta::from_iters([], assets.clone());
1344
1345        let delta = AccountDelta::new(account.id(), storage_delta, vault_delta, ONE)?;
1346
1347        let mut account_after_delta = account.clone();
1348        account_after_delta.apply_delta(&delta)?;
1349
1350        let account_id = account.id();
1351        let final_state: AccountHeader = (&account_after_delta).into();
1352
1353        let merkle_store = store.merkle_store.clone();
1354        store
1355            .interact_with_connection(move |conn| {
1356                let fungible_assets = SqliteStore::get_account_fungible_assets_for_delta(
1357                    conn,
1358                    &(&account).into(),
1359                    &delta,
1360                )?;
1361                let storage_maps = SqliteStore::get_account_storage_maps_for_delta(
1362                    conn,
1363                    &(&account).into(),
1364                    &delta,
1365                )?;
1366                let tx = conn.transaction().into_store_error()?;
1367                let mut merkle_store =
1368                    merkle_store.write().expect("merkle_store write lock not poisoned");
1369
1370                SqliteStore::apply_account_delta(
1371                    &tx,
1372                    &mut merkle_store,
1373                    &account.into(),
1374                    &final_state,
1375                    fungible_assets,
1376                    storage_maps,
1377                    &delta,
1378                )?;
1379
1380                tx.commit().into_store_error()?;
1381                Ok(())
1382            })
1383            .await?;
1384
1385        let updated_account: Account = store
1386            .get_account(account_id)
1387            .await?
1388            .context("failed to find inserted account")?
1389            .into();
1390
1391        assert_eq!(updated_account, account_after_delta);
1392        assert!(updated_account.vault().is_empty());
1393        assert_eq!(updated_account.storage().get_item(1)?, EMPTY_WORD);
1394        let StorageSlot::Map(ref updated_map) = updated_account.storage().slots()[2] else {
1395            panic!("Expected map slot");
1396        };
1397        assert_eq!(updated_map.entries().count(), 0);
1398
1399        Ok(())
1400    }
1401}