Skip to main content

miden_node_store/db/models/queries/
accounts.rs

1use std::collections::{BTreeMap, HashMap};
2use std::num::NonZeroUsize;
3use std::ops::RangeInclusive;
4
5use diesel::prelude::{Queryable, QueryableByName};
6use diesel::query_dsl::methods::SelectDsl;
7use diesel::sqlite::Sqlite;
8use diesel::{
9    BoolExpressionMethods,
10    ExpressionMethods,
11    JoinOnDsl,
12    NullableExpressionMethods,
13    OptionalExtension,
14    QueryDsl,
15    RunQueryDsl,
16    Selectable,
17    SelectableHelper,
18    SqliteConnection,
19};
20use miden_node_proto::domain::account::{AccountInfo, AccountSummary};
21use miden_node_utils::limiter::MAX_RESPONSE_PAYLOAD_BYTES;
22use miden_protocol::Word;
23use miden_protocol::account::{
24    Account,
25    AccountCode,
26    AccountId,
27    AccountStorage,
28    AccountStorageHeader,
29    StorageMap,
30    StorageMapKey,
31    StorageSlot,
32    StorageSlotName,
33    StorageSlotType,
34};
35use miden_protocol::asset::{Asset, AssetId, AssetVault};
36use miden_protocol::block::BlockNumber;
37use miden_protocol::utils::serde::{Deserializable, Serializable};
38
39use crate::db::models::conv::{SqlTypeConvert, raw_sql_to_nonce};
40#[cfg(test)]
41use crate::db::models::vec_raw_try_into;
42use crate::db::{AccountVaultValue, schema};
43use crate::errors::DatabaseError;
44
45type StorageMapValueRow = (i64, String, Vec<u8>, Vec<u8>);
46type StorageHeaderWithEntries =
47    (AccountStorageHeader, HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>>);
48
49/// Sentinel `valid_until` value marking a row as the current, open-ended version of its key.
50///
51/// Versioned rows (`accounts`, `account_vault_assets`, `account_storage_map_values`) are
52/// applicable for blocks in `[block_num, valid_until)`; updating a key closes the previous row's
53/// interval by setting its `valid_until` to the new row's `block_num`. The open end is `i64::MAX`
54/// rather than NULL so every validity predicate is a single range comparison that partial indexes
55/// can serve.
56pub(crate) const VALID_FOREVER: i64 = i64::MAX;
57
58// NETWORK ACCOUNT TYPE
59// ================================================================================================
60
61/// Classifies accounts for database storage based on whether they are network accounts.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub(crate) enum NetworkAccountType {
64    /// Not a network account.
65    None,
66    /// A network account.
67    Network,
68}
69
70// ACCOUNT CODE
71// ================================================================================================
72
73/// Select account code by its commitment hash from the `account_codes` table.
74///
75/// # Returns
76///
77/// The account code bytes if found, or `None` if no code exists with that commitment.
78///
79/// # Raw SQL
80///
81/// ```sql
82/// SELECT code FROM account_codes WHERE code_commitment = ?1
83/// ```
84pub(crate) fn select_account_code_by_commitment(
85    conn: &mut SqliteConnection,
86    code_commitment: Word,
87) -> Result<Option<Vec<u8>>, DatabaseError> {
88    use schema::account_codes;
89
90    let code_commitment_bytes = code_commitment.to_bytes();
91
92    let result: Option<Vec<u8>> = SelectDsl::select(
93        account_codes::table.filter(account_codes::code_commitment.eq(&code_commitment_bytes)),
94        account_codes::code,
95    )
96    .first(conn)
97    .optional()?;
98
99    Ok(result)
100}
101
102// ACCOUNT RETRIEVAL
103// ================================================================================================
104
105/// Select account by ID from the DB using the given [`SqliteConnection`].
106///
107/// # Returns
108///
109/// The latest account info, or an error.
110///
111/// # Raw SQL
112///
113/// ```sql
114/// SELECT
115///     accounts.account_id,
116///     accounts.account_commitment,
117///     accounts.block_num
118/// FROM
119///     accounts
120/// WHERE
121///     account_id = ?1
122///     AND valid_until = {VALID_FOREVER}
123/// ```
124pub(crate) fn select_account(
125    conn: &mut SqliteConnection,
126    account_id: AccountId,
127) -> Result<AccountInfo, DatabaseError> {
128    let raw = SelectDsl::select(schema::accounts::table, AccountSummaryRaw::as_select())
129        .filter(schema::accounts::account_id.eq(account_id.to_bytes()))
130        .filter(schema::accounts::valid_until.eq(VALID_FOREVER))
131        .get_result::<AccountSummaryRaw>(conn)
132        .optional()?
133        .ok_or(DatabaseError::AccountNotFoundInDb(account_id))?;
134
135    let summary: AccountSummary = raw.try_into()?;
136
137    // Public accounts store full details. Private accounts store only the summary.
138    let details = if account_id.is_public() {
139        Some(select_full_account(conn, account_id)?)
140    } else {
141        None
142    };
143
144    Ok(AccountInfo { summary, details })
145}
146
147/// Reconstructs the latest account state from database tables.
148///
149/// Reads code from `account_codes`, the nonce and storage header from `accounts`, map entries from
150/// `account_storage_map_values`, and assets from `account_vault_assets`.
151pub(crate) fn select_full_account(
152    conn: &mut SqliteConnection,
153    account_id: AccountId,
154) -> Result<Account, DatabaseError> {
155    // Get account metadata (nonce, code_commitment) and code in a single join query
156    let joined = schema::accounts::table.inner_join(schema::account_codes::table.on(
157        schema::accounts::code_commitment.eq(schema::account_codes::code_commitment.nullable()),
158    ));
159
160    let (nonce, code_bytes): (Option<i64>, Vec<u8>) =
161        SelectDsl::select(joined, (schema::accounts::nonce, schema::account_codes::code))
162            .filter(schema::accounts::account_id.eq(account_id.to_bytes()))
163            .filter(schema::accounts::valid_until.eq(VALID_FOREVER))
164            .get_result(conn)
165            .optional()?
166            .ok_or(DatabaseError::AccountNotFoundInDb(account_id))?;
167
168    let nonce = raw_sql_to_nonce(nonce.ok_or_else(|| {
169        DatabaseError::DataCorrupted(format!("No nonce found for account {account_id}"))
170    })?);
171
172    let code = miden_node_persistence::decode::<AccountCode>(&code_bytes)?;
173
174    // Reconstruct storage using existing helper function
175    let storage = select_latest_account_storage(conn, account_id)?;
176
177    // Reconstruct vault from account_vault_assets table
178    let vault_entries: Vec<(Vec<u8>, Option<Vec<u8>>)> = SelectDsl::select(
179        schema::account_vault_assets::table,
180        (schema::account_vault_assets::vault_key, schema::account_vault_assets::asset),
181    )
182    .filter(schema::account_vault_assets::account_id.eq(account_id.to_bytes()))
183    .filter(schema::account_vault_assets::valid_until.eq(VALID_FOREVER))
184    .load(conn)?;
185
186    let mut assets = Vec::new();
187    for (_key_bytes, maybe_asset_bytes) in vault_entries {
188        if let Some(asset_bytes) = maybe_asset_bytes {
189            let asset = miden_node_persistence::decode::<Asset>(&asset_bytes)?;
190            assets.push(asset);
191        }
192    }
193
194    let vault = AssetVault::new(&assets)?;
195
196    Ok(Account::new(account_id, vault, storage, code, nonce, None)?)
197}
198
199/// Page of account commitments returned by [`select_account_commitments_paged`].
200#[derive(Debug)]
201pub struct AccountCommitmentsPage {
202    /// The account commitments in this page.
203    pub commitments: Vec<(AccountId, Word)>,
204    /// If `Some`, there are more results. Use this as the `after_account_id` for the next page.
205    pub next_cursor: Option<AccountId>,
206}
207
208/// Selects account commitments with pagination.
209///
210/// Returns up to `page_size` account commitments, starting after `after_account_id` if provided.
211/// Results are ordered by `account_id` for stable pagination.
212///
213/// # Raw SQL
214///
215/// ```sql
216/// SELECT
217///     account_id,
218///     account_commitment
219/// FROM
220///     accounts
221/// WHERE
222///     valid_until = {VALID_FOREVER}
223///     AND (account_id > :after_account_id OR :after_account_id IS NULL)
224/// ORDER BY
225///     account_id ASC
226/// LIMIT :page_size + 1
227/// ```
228pub(crate) fn select_account_commitments_paged(
229    conn: &mut SqliteConnection,
230    page_size: NonZeroUsize,
231    after_account_id: Option<AccountId>,
232) -> Result<AccountCommitmentsPage, DatabaseError> {
233    // Fetch one extra to determine if there are more results
234    #[expect(clippy::cast_possible_wrap)]
235    let limit = (page_size.get() + 1) as i64;
236
237    let mut query = SelectDsl::select(
238        schema::accounts::table,
239        (schema::accounts::account_id, schema::accounts::account_commitment),
240    )
241    .filter(schema::accounts::valid_until.eq(VALID_FOREVER))
242    .order_by(schema::accounts::account_id.asc())
243    .limit(limit)
244    .into_boxed();
245
246    if let Some(cursor) = after_account_id {
247        query = query.filter(schema::accounts::account_id.gt(cursor.to_bytes()));
248    }
249
250    let raw = query.load::<(Vec<u8>, Vec<u8>)>(conn)?;
251
252    let mut commitments = raw
253        .into_iter()
254        .map(|(ref account, ref commitment)| {
255            Ok((AccountId::read_from_bytes(account)?, Word::read_from_bytes(commitment)?))
256        })
257        .collect::<Result<Vec<_>, DatabaseError>>()?;
258
259    // If we got more than page_size, there are more results
260    let next_cursor = if commitments.len() > page_size.get() {
261        commitments.pop(); // Remove the extra element
262        commitments.last().map(|(id, _)| *id)
263    } else {
264        None
265    };
266
267    Ok(AccountCommitmentsPage { commitments, next_cursor })
268}
269
270/// Page of public account IDs returned by [`select_public_account_ids_paged`].
271#[derive(Debug)]
272pub struct PublicAccountIdsPage {
273    /// The public account IDs in this page.
274    pub account_ids: Vec<AccountId>,
275    /// If `Some`, there are more results. Use this as the `after_account_id` for the next page.
276    pub next_cursor: Option<AccountId>,
277}
278
279/// Latest account state forest roots for a public account.
280#[derive(Debug)]
281pub struct PublicAccountStateRoots {
282    pub account_id: AccountId,
283    pub vault_root: Word,
284    pub storage_header: AccountStorageHeader,
285}
286
287/// Page of public account state roots returned by [`select_public_account_state_roots_paged`].
288#[derive(Debug)]
289pub struct PublicAccountStateRootsPage {
290    /// The public account state roots in this page.
291    pub accounts: Vec<PublicAccountStateRoots>,
292    /// If `Some`, there are more results. Use this as the `after_account_id` for the next page.
293    pub next_cursor: Option<AccountId>,
294}
295
296/// Selects public account IDs with pagination.
297///
298/// Returns up to `page_size` public account IDs, starting after `after_account_id` if provided.
299/// Results are ordered by `account_id` for stable pagination.
300///
301/// Public accounts are those with `AccountType::Public`. We identify them by checking
302/// against the store. Public accounts store their `code_commitment`, while private accounts only
303/// store the `account_commitment`.
304///
305/// # Raw SQL
306///
307/// ```sql
308/// SELECT
309///     account_id
310/// FROM
311///     accounts
312/// WHERE
313///     valid_until = {VALID_FOREVER}
314///     AND code_commitment IS NOT NULL
315///     AND (account_id > :after_account_id OR :after_account_id IS NULL)
316/// ORDER BY
317///     account_id ASC
318/// LIMIT :page_size + 1
319/// ```
320pub(crate) fn select_public_account_ids_paged(
321    conn: &mut SqliteConnection,
322    page_size: NonZeroUsize,
323    after_account_id: Option<AccountId>,
324) -> Result<PublicAccountIdsPage, DatabaseError> {
325    #[expect(clippy::cast_possible_wrap)]
326    let limit = (page_size.get() + 1) as i64;
327
328    let mut query = SelectDsl::select(schema::accounts::table, schema::accounts::account_id)
329        .filter(schema::accounts::valid_until.eq(VALID_FOREVER))
330        .filter(schema::accounts::code_commitment.is_not_null())
331        .order_by(schema::accounts::account_id.asc())
332        .limit(limit)
333        .into_boxed();
334
335    if let Some(cursor) = after_account_id {
336        query = query.filter(schema::accounts::account_id.gt(cursor.to_bytes()));
337    }
338
339    let raw = query.load::<Vec<u8>>(conn)?;
340
341    let mut account_ids: Vec<AccountId> = raw
342        .into_iter()
343        .map(|bytes| {
344            AccountId::read_from_bytes(&bytes).map_err(DatabaseError::DeserializationError)
345        })
346        .collect::<Result<_, _>>()?;
347
348    // If we got more than page_size, there are more results
349    let next_cursor = if account_ids.len() > page_size.get() {
350        account_ids.pop(); // Remove the extra element
351        account_ids.last().copied()
352    } else {
353        None
354    };
355
356    Ok(PublicAccountIdsPage { account_ids, next_cursor })
357}
358
359/// Selects public account vault roots and storage headers with pagination.
360///
361/// Returns up to `page_size` public account states, starting after `after_account_id` if provided.
362/// Results are ordered by `account_id` for stable pagination.
363///
364/// Public accounts are those with `AccountType::Public`. We identify them by checking
365/// against the store. Public accounts store their `code_commitment`, while private accounts only
366/// store the `account_commitment`.
367///
368/// # Raw SQL
369///
370/// ```sql
371/// SELECT
372///     account_id,
373///     vault_root,
374///     storage_header
375/// FROM
376///     accounts
377/// WHERE
378///     valid_until = {VALID_FOREVER}
379///     AND code_commitment IS NOT NULL
380///     AND (account_id > :after_account_id OR :after_account_id IS NULL)
381/// ORDER BY
382///     account_id ASC
383/// LIMIT :page_size + 1
384/// ```
385pub(crate) fn select_public_account_state_roots_paged(
386    conn: &mut SqliteConnection,
387    page_size: NonZeroUsize,
388    after_account_id: Option<AccountId>,
389) -> Result<PublicAccountStateRootsPage, DatabaseError> {
390    #[expect(clippy::cast_possible_wrap)]
391    let limit = (page_size.get() + 1) as i64;
392
393    let mut query = SelectDsl::select(
394        schema::accounts::table,
395        (
396            schema::accounts::account_id,
397            schema::accounts::vault_root,
398            schema::accounts::storage_header,
399        ),
400    )
401    .filter(schema::accounts::valid_until.eq(VALID_FOREVER))
402    .filter(schema::accounts::code_commitment.is_not_null())
403    .order_by(schema::accounts::account_id.asc())
404    .limit(limit)
405    .into_boxed();
406
407    if let Some(cursor) = after_account_id {
408        query = query.filter(schema::accounts::account_id.gt(cursor.to_bytes()));
409    }
410
411    let raw = query.load::<(Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>)>(conn)?;
412
413    let mut accounts: Vec<PublicAccountStateRoots> = raw
414        .into_iter()
415        .map(|(account_id_bytes, vault_root_bytes, storage_header_bytes)| {
416            let account_id = AccountId::read_from_bytes(&account_id_bytes)
417                .map_err(DatabaseError::DeserializationError)?;
418            let vault_root_bytes = vault_root_bytes.ok_or_else(|| {
419                DatabaseError::DataCorrupted(format!(
420                    "public account {account_id} is missing a vault root"
421                ))
422            })?;
423            let storage_header_bytes = storage_header_bytes.ok_or_else(|| {
424                DatabaseError::DataCorrupted(format!(
425                    "public account {account_id} is missing a storage header"
426                ))
427            })?;
428
429            Ok::<_, DatabaseError>(PublicAccountStateRoots {
430                account_id,
431                vault_root: Word::read_from_bytes(&vault_root_bytes)?,
432                storage_header: miden_node_persistence::decode::<AccountStorageHeader>(
433                    &storage_header_bytes,
434                )?,
435            })
436        })
437        .collect::<Result<_, _>>()?;
438
439    // If we got more than page_size, there are more results.
440    let next_cursor = if accounts.len() > page_size.get() {
441        accounts.pop();
442        accounts.last().map(|account| account.account_id)
443    } else {
444        None
445    };
446
447    Ok(PublicAccountStateRootsPage { accounts, next_cursor })
448}
449
450/// Select account vault assets within a block range (inclusive).
451///
452/// # Parameters
453/// * `account_id`: Account ID to query
454/// * `block_from`: Starting block number
455/// * `block_to`: Ending block number
456/// * Response payload size: 0 <= size <= 2MB
457/// * Vault assets per response: 0 <= count <= (2MB / (2*Word + u32)) + 1
458///
459/// # Raw SQL
460///
461/// ```sql
462/// SELECT
463///     block_num,
464///     vault_key,
465///     asset
466/// FROM
467///     account_vault_assets
468/// WHERE
469///     account_id = ?1
470///     AND block_num >= ?2
471///     AND block_num <= ?3
472/// ORDER BY
473///     block_num ASC
474/// LIMIT
475///     ?4
476/// ```
477pub(crate) fn select_account_vault_assets(
478    conn: &mut SqliteConnection,
479    account_id: AccountId,
480    block_range: RangeInclusive<BlockNumber>,
481) -> Result<(BlockNumber, Vec<AccountVaultValue>), DatabaseError> {
482    use schema::account_vault_assets as t;
483    // The protocol does not define these limits. Derive a conservative row limit from the response
484    // payload limit.
485    const ROW_OVERHEAD_BYTES: usize = 2 * size_of::<Word>() + size_of::<u32>(); // key + asset + block_num
486    const MAX_ROWS: usize = MAX_RESPONSE_PAYLOAD_BYTES / ROW_OVERHEAD_BYTES;
487
488    if !account_id.is_public() {
489        return Err(DatabaseError::AccountNotPublic(account_id));
490    }
491
492    if block_range.is_empty() {
493        return Err(DatabaseError::InvalidBlockRange {
494            from: *block_range.start(),
495            to: *block_range.end(),
496        });
497    }
498
499    let raw: Vec<(i64, Vec<u8>, Option<Vec<u8>>)> =
500        SelectDsl::select(t::table, (t::block_num, t::vault_key, t::asset))
501            .filter(
502                t::account_id
503                    .eq(account_id.to_bytes())
504                    .and(t::block_num.ge(block_range.start().to_raw_sql()))
505                    .and(t::block_num.le(block_range.end().to_raw_sql())),
506            )
507            .order(t::block_num.asc())
508            .limit(i64::try_from(MAX_ROWS + 1).expect("should fit within i64"))
509            .load::<(i64, Vec<u8>, Option<Vec<u8>>)>(conn)?;
510
511    // If we got more rows than the limit, the last block may be incomplete so we drop it entirely
512    // and derive last_block_included from the remaining rows.
513    let (last_block_included, values) = if let Some(&(last_block_num, ..)) = raw.last()
514        && raw.len() > MAX_ROWS
515    {
516        let values = raw
517            .into_iter()
518            .take_while(|(bn, ..)| *bn != last_block_num)
519            .map(AccountVaultValue::from_raw_row)
520            .collect::<Result<Vec<_>, DatabaseError>>()?;
521
522        let last_block_included = values.last().map_or(*block_range.start(), |v| v.block_num);
523
524        (last_block_included, values)
525    } else {
526        (
527            *block_range.end(),
528            raw.into_iter().map(AccountVaultValue::from_raw_row).collect::<Result<_, _>>()?,
529        )
530    };
531
532    Ok((last_block_included, values))
533}
534
535/// Select all accounts from the DB using the given [`SqliteConnection`].
536///
537/// # Returns
538///
539/// A vector with accounts, or an error.
540///
541/// # Raw SQL
542///
543/// ```sql
544/// SELECT
545///     accounts.account_id,
546///     accounts.account_commitment,
547///     accounts.block_num
548/// FROM
549///     accounts
550/// WHERE
551///     valid_until = {VALID_FOREVER}
552/// ORDER BY
553///     block_num ASC
554/// ```
555#[cfg(test)]
556pub(crate) fn select_all_accounts(
557    conn: &mut SqliteConnection,
558) -> Result<Vec<AccountInfo>, DatabaseError> {
559    let raw = SelectDsl::select(schema::accounts::table, AccountSummaryRaw::as_select())
560        .filter(schema::accounts::valid_until.eq(VALID_FOREVER))
561        .order_by(schema::accounts::block_num.asc())
562        .load::<AccountSummaryRaw>(conn)?;
563
564    let summaries: Vec<AccountSummary> = vec_raw_try_into(raw)?;
565
566    // Backfill account details from database
567    let account_infos = summaries
568        .into_iter()
569        .map(|summary| {
570            let account_id = summary.account_id;
571            let details = select_full_account(conn, account_id).ok();
572            AccountInfo { summary, details }
573        })
574        .collect();
575
576    Ok(account_infos)
577}
578
579#[derive(Debug, Clone, PartialEq, Eq)]
580pub struct StorageMapValue {
581    pub block_num: BlockNumber,
582    pub slot_name: StorageSlotName,
583    pub key: StorageMapKey,
584    pub value: Word,
585}
586
587#[derive(Debug, Clone, PartialEq, Eq)]
588pub struct StorageMapValuesPage {
589    /// Highest block number included in `rows`. If the page is empty, this will be `block_from`.
590    pub last_block_included: BlockNumber,
591    /// Storage map values
592    pub values: Vec<StorageMapValue>,
593}
594
595impl StorageMapValue {
596    pub fn from_raw_row(row: StorageMapValueRow) -> Result<Self, DatabaseError> {
597        let (block_num, slot_name, key, value) = row;
598        Ok(Self {
599            block_num: BlockNumber::from_raw_sql(block_num)?,
600            slot_name: StorageSlotName::from_raw_sql(slot_name)?,
601            key: StorageMapKey::read_from_bytes(&key)?,
602            value: Word::read_from_bytes(&value)?,
603        })
604    }
605}
606
607/// Select account storage map values from the DB using the given [`SqliteConnection`].
608///
609/// # Returns
610///
611/// A vector of tuples containing `(block_num, slot, key, value)` for the given account.
612/// Each row contains one of:
613///
614/// - the historical value for a slot and key specifically on block `block_to`
615/// - the latest updated value for the slot and key combination, alongside the block number in which
616///   it was updated
617///
618/// # Raw SQL
619///
620/// ```sql
621/// SELECT
622///     block_num,
623///     slot,
624///     key,
625///     value
626/// FROM
627///     account_storage_map_values
628/// WHERE
629///     account_id = ?1
630///     AND block_num >= ?2
631///     AND block_num <= ?3
632/// ORDER BY
633///     block_num ASC
634/// LIMIT
635///     ?4
636/// ```
637/// Select account storage map values within a block range (inclusive).
638///
639/// ## Parameters
640///
641/// * `account_id`: Account ID to query
642/// * `block_range`: Range of block numbers (inclusive)
643///
644/// ## Response
645///
646/// * Response payload size: 0 <= size <= 2MB
647/// * Storage map values per response: 0 <= count <= (2MB / (2*Word + u32 + u8)) + 1
648pub(crate) fn select_account_storage_map_values_paged(
649    conn: &mut SqliteConnection,
650    account_id: AccountId,
651    block_range: RangeInclusive<BlockNumber>,
652    limit: usize,
653) -> Result<StorageMapValuesPage, DatabaseError> {
654    use schema::account_storage_map_values as t;
655
656    if !account_id.is_public() {
657        return Err(DatabaseError::AccountNotPublic(account_id));
658    }
659
660    if block_range.is_empty() {
661        return Err(DatabaseError::InvalidBlockRange {
662            from: *block_range.start(),
663            to: *block_range.end(),
664        });
665    }
666
667    let raw: Vec<StorageMapValueRow> =
668        SelectDsl::select(t::table, (t::block_num, t::slot_name, t::key, t::value))
669            .filter(
670                t::account_id
671                    .eq(account_id.to_bytes())
672                    .and(t::block_num.ge(block_range.start().to_raw_sql()))
673                    .and(t::block_num.le(block_range.end().to_raw_sql())),
674            )
675            .order(t::block_num.asc())
676            .limit(i64::try_from(limit + 1).expect("limit fits within i64"))
677            .load(conn)?;
678
679    // If we got more rows than the limit, the last block may be incomplete so we drop it entirely
680    // and derive last_block_included from the remaining rows.
681    let (last_block_included, values) = if let Some(&(last_block_num, ..)) = raw.last()
682        && raw.len() > limit
683    {
684        let values = raw
685            .into_iter()
686            .take_while(|(bn, ..)| *bn != last_block_num)
687            .map(StorageMapValue::from_raw_row)
688            .collect::<Result<Vec<_>, DatabaseError>>()?;
689
690        let last_block_included = values.last().map_or(*block_range.start(), |v| v.block_num);
691
692        (last_block_included, values)
693    } else {
694        (
695            *block_range.end(),
696            raw.into_iter()
697                .map(StorageMapValue::from_raw_row)
698                .collect::<Result<Vec<_>, _>>()?,
699        )
700    };
701
702    Ok(StorageMapValuesPage { last_block_included, values })
703}
704
705/// Select latest account storage by querying `accounts.storage_header` for the account's
706/// open-ended row and reconstructing full storage from the header plus map values from
707/// `account_storage_map_values`.
708///
709/// Attention: For large accounts it is prohibitively expensive!
710pub(crate) fn select_latest_account_storage(
711    conn: &mut SqliteConnection,
712    account_id: AccountId,
713) -> Result<AccountStorage, DatabaseError> {
714    let (storage_header, map_entries_by_slot) =
715        select_latest_account_storage_components(conn, account_id)?;
716    // Reconstruct StorageSlots from header slots + map entries
717    let slots = storage_header
718        .slots()
719        .map(|slot_header| {
720            let slot = match slot_header.slot_type() {
721                StorageSlotType::Value => {
722                    // For value slots, the header value IS the slot value
723                    StorageSlot::with_value(slot_header.name().clone(), slot_header.value())
724                },
725                StorageSlotType::Map => {
726                    // For map slots, reconstruct from map entries
727                    let entries =
728                        map_entries_by_slot.get(slot_header.name()).cloned().unwrap_or_default();
729                    let storage_map = StorageMap::with_entries(entries)?;
730                    StorageSlot::with_map(slot_header.name().clone(), storage_map)
731                },
732            };
733            Ok(slot)
734        })
735        .collect::<Result<Vec<_>, DatabaseError>>()?;
736
737    Ok(AccountStorage::new(slots)?)
738}
739
740/// Fetch account storage header and all storage maps
741pub(crate) fn select_latest_account_storage_components(
742    conn: &mut SqliteConnection,
743    account_id: AccountId,
744) -> Result<StorageHeaderWithEntries, DatabaseError> {
745    let account_id_bytes = account_id.to_bytes();
746
747    // Query storage header blob for this account's current (open-ended) row
748    let storage_blob: Option<Vec<u8>> =
749        SelectDsl::select(schema::accounts::table, schema::accounts::storage_header)
750            .filter(schema::accounts::account_id.eq(&account_id_bytes))
751            .filter(schema::accounts::valid_until.eq(VALID_FOREVER))
752            .first(conn)
753            .optional()?
754            .flatten();
755
756    let header = match storage_blob {
757        Some(blob) => miden_node_persistence::decode::<AccountStorageHeader>(&blob)?,
758        None => AccountStorageHeader::new(Vec::new())?,
759    };
760
761    let entries = select_latest_storage_map_entries_all(conn, &account_id)?;
762    Ok((header, entries))
763}
764
765// This query is expensive because it loads every current storage map entry for the account.
766fn select_latest_storage_map_entries_all(
767    conn: &mut SqliteConnection,
768    account_id: &AccountId,
769) -> Result<HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>>, DatabaseError> {
770    use schema::account_storage_map_values as t;
771
772    let map_values: Vec<(String, Vec<u8>, Vec<u8>)> =
773        SelectDsl::select(t::table, (t::slot_name, t::key, t::value))
774            .filter(t::account_id.eq(&account_id.to_bytes()))
775            .filter(t::valid_until.eq(VALID_FOREVER))
776            .load(conn)?;
777
778    group_storage_map_entries(map_values)
779}
780
781fn group_storage_map_entries(
782    map_values: Vec<(String, Vec<u8>, Vec<u8>)>,
783) -> Result<HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>>, DatabaseError> {
784    let mut map_entries_by_slot: HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>> =
785        HashMap::new();
786    for (slot_name_str, key_bytes, value_bytes) in map_values {
787        let slot_name: StorageSlotName = slot_name_str.parse().map_err(|_| {
788            DatabaseError::DataCorrupted(format!("Invalid slot name: {slot_name_str}"))
789        })?;
790        let key = StorageMapKey::read_from_bytes(&key_bytes)?;
791        let value = Word::read_from_bytes(&value_bytes)?;
792        map_entries_by_slot.entry(slot_name).or_default().insert(key, value);
793    }
794
795    Ok(map_entries_by_slot)
796}
797
798// ACCOUNT MUTATION
799// ================================================================================================
800
801#[derive(Queryable, Selectable)]
802#[diesel(table_name = crate::db::schema::account_vault_assets)]
803#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
804pub struct AccountVaultUpdateRaw {
805    pub vault_key: Vec<u8>,
806    pub asset: Option<Vec<u8>>,
807    pub block_num: i64,
808}
809
810impl TryFrom<AccountVaultUpdateRaw> for AccountVaultValue {
811    type Error = DatabaseError;
812
813    fn try_from(raw: AccountVaultUpdateRaw) -> Result<Self, Self::Error> {
814        let vault_key = AssetId::try_from(Word::read_from_bytes(&raw.vault_key)?)?;
815        let asset = raw
816            .asset
817            .map(|bytes| miden_node_persistence::decode::<Asset>(&bytes))
818            .transpose()?;
819        let block_num = BlockNumber::from_raw_sql(raw.block_num)?;
820
821        Ok(AccountVaultValue { block_num, vault_key, asset })
822    }
823}
824
825#[derive(Debug, Clone, PartialEq, Eq, Selectable, Queryable, QueryableByName)]
826#[diesel(table_name = schema::accounts)]
827#[diesel(check_for_backend(Sqlite))]
828pub struct AccountSummaryRaw {
829    account_id: Vec<u8>,         // AccountId,
830    account_commitment: Vec<u8>, //RpoDigest,
831    block_num: i64,              //BlockNumber,
832}
833
834impl TryInto<AccountSummary> for AccountSummaryRaw {
835    type Error = DatabaseError;
836    fn try_into(self) -> Result<AccountSummary, Self::Error> {
837        let account_id = AccountId::read_from_bytes(&self.account_id[..])?;
838        let account_commitment = Word::read_from_bytes(&self.account_commitment[..])?;
839        let block_num = BlockNumber::from_raw_sql(self.block_num)?;
840
841        Ok(AccountSummary {
842            account_id,
843            account_commitment,
844            block_num,
845        })
846    }
847}