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::crypto::MerkleStore;
10use miden_client::note::ToInputNoteCommitments;
11use miden_client::store::{StoreError, TransactionFilter};
12use miden_client::transaction::{
13    TransactionDetails,
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    pub fn apply_transaction(
108        conn: &mut Connection,
109        merkle_store: &Arc<RwLock<MerkleStore>>,
110        tx_update: &TransactionStoreUpdate,
111    ) -> Result<(), StoreError> {
112        let executed_transaction = tx_update.executed_transaction();
113
114        let updated_fungible_assets = Self::get_account_fungible_assets_for_delta(
115            conn,
116            &executed_transaction.initial_account().into(),
117            executed_transaction.account_delta(),
118        )?;
119
120        let updated_storage_maps = Self::get_account_storage_maps_for_delta(
121            conn,
122            &executed_transaction.initial_account().into(),
123            executed_transaction.account_delta(),
124        )?;
125
126        let tx = conn.transaction().into_store_error()?;
127
128        // Build transaction record
129        let nullifiers: Vec<Word> = executed_transaction
130            .input_notes()
131            .iter()
132            .map(|x| x.nullifier().as_word())
133            .collect();
134
135        let output_notes = executed_transaction.output_notes();
136
137        let details = TransactionDetails {
138            account_id: executed_transaction.account_id(),
139            init_account_state: executed_transaction.initial_account().commitment(),
140            final_account_state: executed_transaction.final_account().commitment(),
141            input_note_nullifiers: nullifiers,
142            output_notes: output_notes.clone(),
143            block_num: executed_transaction.block_header().block_num(),
144            submission_height: tx_update.submission_height(),
145            expiration_block_num: executed_transaction.expiration_block_num(),
146            creation_timestamp: super::current_timestamp_u64(),
147        };
148
149        let transaction_record = TransactionRecord::new(
150            executed_transaction.id(),
151            details,
152            executed_transaction.tx_args().tx_script().cloned(),
153            TransactionStatus::Pending,
154        );
155
156        // Insert transaction data
157        upsert_transaction_record(&tx, &transaction_record)?;
158
159        // Account Data
160        let mut merkle_store = merkle_store.write().expect("merkle_store write lock not poisoned");
161        Self::apply_account_delta(
162            &tx,
163            &mut merkle_store,
164            &executed_transaction.initial_account().into(),
165            executed_transaction.final_account(),
166            updated_fungible_assets,
167            updated_storage_maps,
168            executed_transaction.account_delta(),
169        )?;
170        drop(merkle_store);
171
172        // Note Updates
173        apply_note_updates_tx(&tx, tx_update.note_updates())?;
174
175        // Note tags
176        for tag_record in tx_update.new_tags() {
177            add_note_tag_tx(&tx, tag_record)?;
178        }
179
180        tx.commit().into_store_error()?;
181
182        Ok(())
183    }
184}
185
186/// Updates the transaction record in the database, inserting it if it doesn't exist.
187pub(crate) fn upsert_transaction_record(
188    tx: &Transaction<'_>,
189    transaction: &TransactionRecord,
190) -> Result<(), StoreError> {
191    let SerializedTransactionData {
192        id,
193        script_root,
194        tx_script,
195        details,
196        block_num,
197        status_variant,
198        status,
199    } = serialize_transaction_data(transaction);
200
201    if let Some(root) = script_root.clone() {
202        tx.execute(INSERT_TRANSACTION_SCRIPT_QUERY, params![root, tx_script])
203            .into_store_error()?;
204    }
205
206    tx.execute(
207        UPSERT_TRANSACTION_QUERY,
208        params![id, details, script_root, block_num, status_variant, status],
209    )
210    .into_store_error()?;
211
212    Ok(())
213}
214
215/// Serializes the transaction record into a format suitable for storage in the database.
216fn serialize_transaction_data(transaction_record: &TransactionRecord) -> SerializedTransactionData {
217    let transaction_id: String = transaction_record.id.to_hex();
218
219    let script_root = transaction_record.script.as_ref().map(|script| script.root().to_bytes());
220    let tx_script = transaction_record.script.as_ref().map(TransactionScript::to_bytes);
221
222    SerializedTransactionData {
223        id: transaction_id,
224        script_root,
225        tx_script,
226        details: transaction_record.details.to_bytes(),
227        block_num: transaction_record.details.block_num.as_u32(),
228        status_variant: transaction_record.status.variant() as u8,
229        status: transaction_record.status.to_bytes(),
230    }
231}
232
233fn parse_transaction_columns(
234    row: &rusqlite::Row<'_>,
235) -> Result<SerializedTransactionParts, rusqlite::Error> {
236    let id: String = row.get(0)?;
237    let tx_script: Option<Vec<u8>> = row.get(1)?;
238    let details: Vec<u8> = row.get(2)?;
239    let status: Vec<u8> = row.get(3)?;
240
241    Ok(SerializedTransactionParts { id, tx_script, details, status })
242}
243
244/// Parse a transaction from the provided parts.
245fn parse_transaction(
246    serialized_transaction: SerializedTransactionParts,
247) -> Result<TransactionRecord, StoreError> {
248    let SerializedTransactionParts { id, tx_script, details, status } = serialized_transaction;
249
250    let id: Word = id.as_str().try_into()?;
251
252    let script: Option<TransactionScript> = tx_script
253        .map(|script| TransactionScript::read_from_bytes(&script))
254        .transpose()?;
255
256    Ok(TransactionRecord {
257        id: id.into(),
258        details: TransactionDetails::read_from_bytes(&details)?,
259        script,
260        status: TransactionStatus::read_from_bytes(&status)?,
261    })
262}