Skip to main content

miden_client_sqlite_store/
transaction.rs

1#![allow(clippy::items_after_statements)]
2
3use std::rc::Rc;
4use std::string::{String, ToString};
5use std::sync::{Arc, RwLock};
6use std::vec::Vec;
7
8use miden_client::Word;
9use miden_client::note::ToInputNoteCommitments;
10use miden_client::store::{AccountSmtForest, StoreError, TransactionFilter};
11use miden_client::transaction::{
12    TransactionDetails,
13    TransactionId,
14    TransactionRecord,
15    TransactionScript,
16    TransactionStatus,
17    TransactionStoreUpdate,
18};
19use miden_client::utils::{Deserializable as _, Serializable as _};
20use rusqlite::types::Value;
21use rusqlite::{Connection, Transaction, params};
22
23use super::SqliteStore;
24use super::note::apply_note_updates_tx;
25use super::sync::add_note_tag_tx;
26use crate::sql_error::SqlResultExt;
27use crate::{insert_sql, subst};
28
29pub(crate) const UPSERT_TRANSACTION_QUERY: &str = insert_sql!(
30    transactions {
31        id,
32        details,
33        script_root,
34        block_num,
35        status_variant,
36        status
37    } | REPLACE
38);
39
40pub(crate) const INSERT_TRANSACTION_SCRIPT_QUERY: &str =
41    insert_sql!(transaction_scripts { script_root, script } | IGNORE);
42
43// TRANSACTIONS
44// ================================================================================================
45
46struct SerializedTransactionData {
47    /// Transaction ID
48    id: String,
49    /// Script root
50    script_root: Option<Vec<u8>>,
51    /// Transaction script
52    tx_script: Option<Vec<u8>>,
53    /// Transaction details
54    details: Vec<u8>,
55    /// Block number
56    block_num: u32,
57    /// Transaction status variant identifier
58    status_variant: u8,
59    /// Serialized transaction status
60    status: Vec<u8>,
61}
62
63struct SerializedTransactionParts {
64    /// Transaction ID
65    id: String,
66    /// Transaction script
67    tx_script: Option<Vec<u8>>,
68    /// Transaction details
69    details: Vec<u8>,
70    /// Serialized transaction status
71    status: Vec<u8>,
72}
73
74impl SqliteStore {
75    /// Retrieves tracked transactions, filtered by [`TransactionFilter`].
76    pub fn get_transactions(
77        conn: &mut Connection,
78        filter: &TransactionFilter,
79    ) -> Result<Vec<TransactionRecord>, StoreError> {
80        match filter {
81            TransactionFilter::Ids(ids) => {
82                // Convert transaction IDs to strings for the array parameter
83                let id_strings =
84                    ids.iter().map(|id| Value::Text(id.to_string())).collect::<Vec<_>>();
85
86                // Create a prepared statement and bind the array parameter
87                conn.prepare(filter.to_query().as_ref())
88                    .into_store_error()?
89                    .query_map(params![Rc::new(id_strings)], parse_transaction_columns)
90                    .into_store_error()?
91                    .map(|result| Ok(result.into_store_error()?).and_then(parse_transaction))
92                    .collect::<Result<Vec<TransactionRecord>, _>>()
93            },
94            _ => {
95                // For other filters, no parameters are needed
96                conn.prepare(filter.to_query().as_ref())
97                    .into_store_error()?
98                    .query_map([], parse_transaction_columns)
99                    .into_store_error()?
100                    .map(|result| Ok(result.into_store_error()?).and_then(parse_transaction))
101                    .collect::<Result<Vec<TransactionRecord>, _>>()
102            },
103        }
104    }
105
106    /// Inserts a transaction and updates the current state based on the `tx_result` changes.
107    ///
108    /// SQL writes and `AccountSmtForest` mutations are committed atomically: on any error
109    /// (including commit failure) the rusqlite transaction is rolled back and the in-memory
110    /// forest is left unchanged.
111    pub fn apply_transaction(
112        conn: &mut Connection,
113        smt_forest: &Arc<RwLock<AccountSmtForest>>,
114        tx_update: &TransactionStoreUpdate,
115    ) -> Result<(), StoreError> {
116        with_forest_snapshot(conn, smt_forest, |db_tx, forest| {
117            Self::apply_transaction_in_txn(db_tx, forest, tx_update)
118        })
119    }
120
121    /// Applies a batch of [`TransactionStoreUpdate`]s atomically. Either every update in the
122    /// slice is persisted or none are. Executes in order inside a single
123    /// [`rusqlite::Transaction`]; on any error the transaction is rolled back automatically
124    /// and the in-memory `AccountSmtForest` is left unchanged.
125    pub fn apply_transaction_batch(
126        conn: &mut Connection,
127        smt_forest: &Arc<RwLock<AccountSmtForest>>,
128        tx_updates: &[TransactionStoreUpdate],
129    ) -> Result<(), StoreError> {
130        with_forest_snapshot(conn, smt_forest, |db_tx, forest| {
131            for update in tx_updates {
132                Self::apply_transaction_in_txn(db_tx, forest, update)?;
133            }
134            Ok(())
135        })
136    }
137
138    /// Applies a transaction's store update within the provided rusqlite transaction.
139    /// Does NOT commit — caller is responsible for commit/rollback.
140    ///
141    /// `smt_forest` must be a working snapshot (such as the clone handed out by
142    /// [`with_forest_snapshot`]), never the live forest: this stages root mutations into it
143    /// and provides no rollback of its own, relying on the caller to discard the snapshot on
144    /// error.
145    ///
146    /// Pre-reads (fungible assets and storage map roots) are performed via the transaction so
147    /// that each call sees writes made by prior calls within the same outer transaction.
148    pub(crate) fn apply_transaction_in_txn(
149        db_tx: &mut Transaction<'_>,
150        smt_forest: &mut AccountSmtForest,
151        tx_update: &TransactionStoreUpdate,
152    ) -> Result<(), StoreError> {
153        let executed_transaction = tx_update.executed_transaction();
154
155        let updated_fungible_assets = Self::get_account_fungible_assets_for_delta(
156            db_tx,
157            executed_transaction.account_id(),
158            executed_transaction.account_delta(),
159        )?;
160
161        let old_map_roots = Self::get_storage_map_roots_for_delta(
162            db_tx,
163            executed_transaction.account_id(),
164            executed_transaction.account_delta(),
165        )?;
166
167        // Build transaction record
168        let nullifiers: Vec<Word> = executed_transaction
169            .input_notes()
170            .iter()
171            .map(|x| x.nullifier().as_word())
172            .collect();
173
174        let output_notes = executed_transaction.output_notes();
175
176        let details = TransactionDetails {
177            account_id: executed_transaction.account_id(),
178            init_account_state: executed_transaction.initial_account().initial_commitment(),
179            final_account_state: executed_transaction.final_account().to_commitment(),
180            input_note_nullifiers: nullifiers,
181            output_notes: output_notes.clone(),
182            block_num: executed_transaction.block_header().block_num(),
183            submission_height: tx_update.submission_height(),
184            expiration_block_num: executed_transaction.expiration_block_num(),
185            creation_timestamp: super::current_timestamp_u64(),
186        };
187
188        let transaction_record = TransactionRecord::new(
189            executed_transaction.id(),
190            details,
191            executed_transaction.tx_args().tx_script().cloned(),
192            TransactionStatus::Pending,
193        );
194
195        // Insert transaction data
196        upsert_transaction_record(db_tx, &transaction_record)?;
197
198        // Account Data
199        Self::apply_account_delta(
200            db_tx,
201            smt_forest,
202            &executed_transaction.initial_account().into(),
203            executed_transaction.final_account(),
204            updated_fungible_assets,
205            &old_map_roots,
206            executed_transaction.account_delta(),
207        )?;
208
209        // Note Updates
210        apply_note_updates_tx(db_tx, tx_update.note_updates())?;
211
212        // Note tags
213        for tag_record in tx_update.new_tags() {
214            add_note_tag_tx(db_tx, tag_record)?;
215        }
216
217        Ok(())
218    }
219}
220
221/// Runs `f` inside a SQL transaction with the forest write lock held, mutating a working
222/// clone and swapping it into the live `RwLock` only after the SQL commit succeeds. This
223/// keeps the `AccountSmtForest` and the `SQLite` store atomic: on any error (`?` from `f`
224/// or commit failure) the live forest is untouched, since all mutations target the clone.
225///
226/// Cost: `AccountSmtForest::clone()` deep-clones the whole `SmtForest` (every tracked SMT
227/// plus root refcounts), so this is O(forest size) per call. Acceptable now; revisit (e.g.
228/// an inverse-op journal) if the forest grows large.
229pub(crate) fn with_forest_snapshot<F, T>(
230    conn: &mut Connection,
231    smt_forest: &Arc<RwLock<AccountSmtForest>>,
232    f: F,
233) -> Result<T, StoreError>
234where
235    F: FnOnce(&mut Transaction<'_>, &mut AccountSmtForest) -> Result<T, StoreError>,
236{
237    let mut db_tx = conn.transaction().into_store_error()?;
238    let mut forest = smt_forest
239        .write()
240        .map_err(|_| StoreError::DatabaseError("smt_forest write lock poisoned".to_string()))?;
241    let mut working_forest = forest.clone();
242
243    let value = f(&mut db_tx, &mut working_forest)?;
244    db_tx.commit().into_store_error()?;
245
246    *forest = working_forest;
247    Ok(value)
248}
249
250/// Updates the transaction record in the database, inserting it if it doesn't exist.
251pub(crate) fn upsert_transaction_record(
252    tx: &Transaction<'_>,
253    transaction: &TransactionRecord,
254) -> Result<(), StoreError> {
255    let SerializedTransactionData {
256        id,
257        script_root,
258        tx_script,
259        details,
260        block_num,
261        status_variant,
262        status,
263    } = serialize_transaction_data(transaction);
264
265    if let Some(root) = script_root.clone() {
266        tx.execute(INSERT_TRANSACTION_SCRIPT_QUERY, params![root, tx_script])
267            .into_store_error()?;
268    }
269
270    tx.execute(
271        UPSERT_TRANSACTION_QUERY,
272        params![id, details, script_root, block_num, status_variant, status],
273    )
274    .into_store_error()?;
275
276    Ok(())
277}
278
279/// Serializes the transaction record into a format suitable for storage in the database.
280fn serialize_transaction_data(transaction_record: &TransactionRecord) -> SerializedTransactionData {
281    let transaction_id: String = transaction_record.id.to_hex();
282
283    let script_root = transaction_record.script.as_ref().map(|script| script.root().to_bytes());
284    let tx_script = transaction_record.script.as_ref().map(TransactionScript::to_bytes);
285
286    SerializedTransactionData {
287        id: transaction_id,
288        script_root,
289        tx_script,
290        details: transaction_record.details.to_bytes(),
291        block_num: transaction_record.details.block_num.as_u32(),
292        status_variant: transaction_record.status.variant() as u8,
293        status: transaction_record.status.to_bytes(),
294    }
295}
296
297fn parse_transaction_columns(
298    row: &rusqlite::Row<'_>,
299) -> Result<SerializedTransactionParts, rusqlite::Error> {
300    let id: String = row.get(0)?;
301    let tx_script: Option<Vec<u8>> = row.get(1)?;
302    let details: Vec<u8> = row.get(2)?;
303    let status: Vec<u8> = row.get(3)?;
304
305    Ok(SerializedTransactionParts { id, tx_script, details, status })
306}
307
308/// Parse a transaction from the provided parts.
309fn parse_transaction(
310    serialized_transaction: SerializedTransactionParts,
311) -> Result<TransactionRecord, StoreError> {
312    let SerializedTransactionParts { id, tx_script, details, status } = serialized_transaction;
313
314    let id: Word = id.as_str().try_into()?;
315
316    let script: Option<TransactionScript> = tx_script
317        .map(|script| TransactionScript::read_from_bytes(&script))
318        .transpose()?;
319
320    Ok(TransactionRecord {
321        id: TransactionId::from_raw(id),
322        details: TransactionDetails::read_from_bytes(&details)?,
323        script,
324        status: TransactionStatus::read_from_bytes(&status)?,
325    })
326}