Skip to main content

miden_client_sqlite_store/
transaction.rs

1#![allow(clippy::items_after_statements)]
2
3use std::rc::Rc;
4use std::vec::Vec;
5
6use miden_client::Word;
7use miden_client::note::ToInputNoteCommitments;
8use miden_client::store::{StoreError, TransactionFilter};
9use miden_client::transaction::{
10    TransactionDetails,
11    TransactionId,
12    TransactionRecord,
13    TransactionScript,
14    TransactionStatus,
15    TransactionStoreUpdate,
16};
17use miden_client::utils::{Deserializable as _, Serializable as _};
18use rusqlite::types::Value;
19use rusqlite::{Connection, Transaction, TransactionBehavior, params};
20
21use super::SqliteStore;
22use super::note::apply_note_updates_tx;
23use super::sync::add_note_tag_tx;
24use crate::forest::{ScopedAccountForest, SqliteForestBackend};
25use crate::sql_error::SqlResultExt;
26use crate::{insert_sql, subst};
27
28pub(crate) const UPSERT_TRANSACTION_QUERY: &str = insert_sql!(
29    transactions {
30        id,
31        details,
32        script_root,
33        block_num,
34        status_variant,
35        status
36    } | REPLACE
37);
38
39pub(crate) const INSERT_TRANSACTION_SCRIPT_QUERY: &str =
40    insert_sql!(transaction_scripts { script_root, script } | IGNORE);
41
42// TRANSACTIONS
43// ================================================================================================
44
45struct SerializedTransactionData {
46    /// Transaction ID
47    id: Vec<u8>,
48    /// Script root
49    script_root: Option<Vec<u8>>,
50    /// Transaction script
51    tx_script: Option<Vec<u8>>,
52    /// Transaction details
53    details: Vec<u8>,
54    /// Block number
55    block_num: u32,
56    /// Transaction status variant identifier
57    status_variant: u8,
58    /// Serialized transaction status
59    status: Vec<u8>,
60}
61
62struct SerializedTransactionParts {
63    /// Transaction ID
64    id: Vec<u8>,
65    /// Transaction script
66    tx_script: Option<Vec<u8>>,
67    /// Transaction details
68    details: Vec<u8>,
69    /// Serialized transaction status
70    status: Vec<u8>,
71}
72
73impl SqliteStore {
74    /// Retrieves tracked transactions, filtered by [`TransactionFilter`].
75    pub fn get_transactions(
76        conn: &mut Connection,
77        filter: &TransactionFilter,
78    ) -> Result<Vec<TransactionRecord>, StoreError> {
79        match filter {
80            TransactionFilter::Ids(ids) => {
81                let id_blobs = ids.iter().map(|id| Value::Blob(id.to_bytes())).collect::<Vec<_>>();
82
83                // Create a prepared statement and bind the array parameter
84                conn.prepare(filter.to_query().as_ref())
85                    .into_store_error()?
86                    .query_map(params![Rc::new(id_blobs)], parse_transaction_columns)
87                    .into_store_error()?
88                    .map(|result| Ok(result.into_store_error()?).and_then(parse_transaction))
89                    .collect::<Result<Vec<TransactionRecord>, _>>()
90            },
91            _ => {
92                // For other filters, no parameters are needed
93                conn.prepare(filter.to_query().as_ref())
94                    .into_store_error()?
95                    .query_map([], parse_transaction_columns)
96                    .into_store_error()?
97                    .map(|result| Ok(result.into_store_error()?).and_then(parse_transaction))
98                    .collect::<Result<Vec<TransactionRecord>, _>>()
99            },
100        }
101    }
102
103    /// Inserts a transaction and updates the current state based on the `tx_result` changes.
104    ///
105    /// SQL writes and forest mutations go through the same rusqlite transaction, so they commit
106    /// or roll back atomically.
107    pub fn apply_transaction(
108        conn: &mut Connection,
109        tx_update: &TransactionStoreUpdate,
110    ) -> Result<(), StoreError> {
111        let db_tx = conn
112            .transaction_with_behavior(TransactionBehavior::Immediate)
113            .into_store_error()?;
114        {
115            let mut forest = ScopedAccountForest::new(SqliteForestBackend::new(&db_tx))?;
116            Self::apply_transaction_in_txn(&db_tx, &mut forest, tx_update)?;
117        }
118        db_tx.commit().into_store_error()
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`].
124    pub fn apply_transaction_batch(
125        conn: &mut Connection,
126        tx_updates: &[TransactionStoreUpdate],
127    ) -> Result<(), StoreError> {
128        let db_tx = conn
129            .transaction_with_behavior(TransactionBehavior::Immediate)
130            .into_store_error()?;
131        {
132            let mut forest = ScopedAccountForest::new(SqliteForestBackend::new(&db_tx))?;
133            for update in tx_updates {
134                Self::apply_transaction_in_txn(&db_tx, &mut forest, update)?;
135            }
136        }
137        db_tx.commit().into_store_error()
138    }
139
140    /// Applies a transaction's store update within the provided rusqlite transaction.
141    /// Does NOT commit — caller is responsible for commit/rollback.
142    ///
143    /// The storage-map-root pre-read is performed via the transaction so that each call sees
144    /// writes made by prior calls within the same outer transaction.
145    pub(crate) fn apply_transaction_in_txn(
146        db_tx: &Transaction<'_>,
147        smt_forest: &mut ScopedAccountForest<'_, '_>,
148        tx_update: &TransactionStoreUpdate,
149    ) -> Result<(), StoreError> {
150        let executed_transaction = tx_update.executed_transaction();
151        let account_patch = executed_transaction.account_patch();
152
153        // Build transaction record
154        let nullifiers: Vec<Word> = executed_transaction
155            .input_notes()
156            .iter()
157            .map(|x| x.nullifier().as_word())
158            .collect();
159
160        let output_notes = executed_transaction.output_notes();
161
162        let details = TransactionDetails {
163            account_id: executed_transaction.account_id(),
164            init_account_state: executed_transaction.initial_account().initial_commitment(),
165            final_account_state: executed_transaction.final_account().to_commitment(),
166            input_note_nullifiers: nullifiers,
167            output_notes: output_notes.clone(),
168            block_num: executed_transaction.block_header().block_num(),
169            submission_height: tx_update.submission_height(),
170            expiration_block_num: executed_transaction.expiration_block_num(),
171            creation_timestamp: super::current_timestamp_u64(),
172        };
173
174        let transaction_record = TransactionRecord::new(
175            executed_transaction.id(),
176            details,
177            executed_transaction.tx_args().tx_script().cloned(),
178            TransactionStatus::Pending,
179        );
180
181        // Insert transaction data
182        upsert_transaction_record(db_tx, &transaction_record)?;
183
184        // Account Data
185        Self::apply_account_patch(
186            db_tx,
187            smt_forest,
188            &executed_transaction.initial_account().into(),
189            executed_transaction.final_account(),
190            account_patch,
191        )?;
192
193        // Note Updates
194        apply_note_updates_tx(db_tx, tx_update.note_updates())?;
195
196        // Note tags
197        for tag_record in tx_update.new_tags() {
198            add_note_tag_tx(db_tx, tag_record)?;
199        }
200
201        Ok(())
202    }
203}
204
205/// Updates the transaction record in the database, inserting it if it doesn't exist.
206pub(crate) fn upsert_transaction_record(
207    tx: &Transaction<'_>,
208    transaction: &TransactionRecord,
209) -> Result<(), StoreError> {
210    let SerializedTransactionData {
211        id,
212        script_root,
213        tx_script,
214        details,
215        block_num,
216        status_variant,
217        status,
218    } = serialize_transaction_data(transaction);
219
220    if let Some(root) = script_root.clone() {
221        tx.execute(INSERT_TRANSACTION_SCRIPT_QUERY, params![root, tx_script])
222            .into_store_error()?;
223    }
224
225    tx.execute(
226        UPSERT_TRANSACTION_QUERY,
227        params![id, details, script_root, block_num, status_variant, status],
228    )
229    .into_store_error()?;
230
231    Ok(())
232}
233
234/// Serializes the transaction record into a format suitable for storage in the database.
235fn serialize_transaction_data(transaction_record: &TransactionRecord) -> SerializedTransactionData {
236    let transaction_id = transaction_record.id.to_bytes();
237
238    let script_root = transaction_record.script.as_ref().map(|script| script.root().to_bytes());
239    let tx_script = transaction_record.script.as_ref().map(TransactionScript::to_bytes);
240
241    SerializedTransactionData {
242        id: transaction_id,
243        script_root,
244        tx_script,
245        details: transaction_record.details.to_bytes(),
246        block_num: transaction_record.details.block_num.as_u32(),
247        status_variant: transaction_record.status.variant() as u8,
248        status: transaction_record.status.to_bytes(),
249    }
250}
251
252fn parse_transaction_columns(
253    row: &rusqlite::Row<'_>,
254) -> Result<SerializedTransactionParts, rusqlite::Error> {
255    let id: Vec<u8> = row.get(0)?;
256    let tx_script: Option<Vec<u8>> = row.get(1)?;
257    let details: Vec<u8> = row.get(2)?;
258    let status: Vec<u8> = row.get(3)?;
259
260    Ok(SerializedTransactionParts { id, tx_script, details, status })
261}
262
263/// Parse a transaction from the provided parts.
264fn parse_transaction(
265    serialized_transaction: SerializedTransactionParts,
266) -> Result<TransactionRecord, StoreError> {
267    let SerializedTransactionParts { id, tx_script, details, status } = serialized_transaction;
268
269    let id = TransactionId::read_from_bytes(&id)?;
270
271    let script: Option<TransactionScript> = tx_script
272        .map(|script| TransactionScript::read_from_bytes(&script))
273        .transpose()?;
274
275    Ok(TransactionRecord {
276        id,
277        details: TransactionDetails::read_from_bytes(&details)?,
278        script,
279        status: TransactionStatus::read_from_bytes(&status)?,
280    })
281}