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::{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::smt_forest::AccountSmtForest;
27use crate::sql_error::SqlResultExt;
28use crate::{insert_sql, subst};
29
30pub(crate) const UPSERT_TRANSACTION_QUERY: &str = insert_sql!(
31    transactions {
32        id,
33        details,
34        script_root,
35        block_num,
36        status_variant,
37        status
38    } | REPLACE
39);
40
41pub(crate) const INSERT_TRANSACTION_SCRIPT_QUERY: &str =
42    insert_sql!(transaction_scripts { script_root, script } | IGNORE);
43
44// TRANSACTIONS
45// ================================================================================================
46
47struct SerializedTransactionData {
48    /// Transaction ID
49    id: String,
50    /// Script root
51    script_root: Option<Vec<u8>>,
52    /// Transaction script
53    tx_script: Option<Vec<u8>>,
54    /// Transaction details
55    details: Vec<u8>,
56    /// Block number
57    block_num: u32,
58    /// Transaction status variant identifier
59    status_variant: u8,
60    /// Serialized transaction status
61    status: Vec<u8>,
62}
63
64struct SerializedTransactionParts {
65    /// Transaction ID
66    id: String,
67    /// Transaction script
68    tx_script: Option<Vec<u8>>,
69    /// Transaction details
70    details: Vec<u8>,
71    /// Serialized transaction status
72    status: Vec<u8>,
73}
74
75impl SqliteStore {
76    /// Retrieves tracked transactions, filtered by [`TransactionFilter`].
77    pub fn get_transactions(
78        conn: &mut Connection,
79        filter: &TransactionFilter,
80    ) -> Result<Vec<TransactionRecord>, StoreError> {
81        match filter {
82            TransactionFilter::Ids(ids) => {
83                // Convert transaction IDs to strings for the array parameter
84                let id_strings =
85                    ids.iter().map(|id| Value::Text(id.to_string())).collect::<Vec<_>>();
86
87                // Create a prepared statement and bind the array parameter
88                conn.prepare(filter.to_query().as_ref())
89                    .into_store_error()?
90                    .query_map(params![Rc::new(id_strings)], parse_transaction_columns)
91                    .into_store_error()?
92                    .map(|result| Ok(result.into_store_error()?).and_then(parse_transaction))
93                    .collect::<Result<Vec<TransactionRecord>, _>>()
94            },
95            _ => {
96                // For other filters, no parameters are needed
97                conn.prepare(filter.to_query().as_ref())
98                    .into_store_error()?
99                    .query_map([], parse_transaction_columns)
100                    .into_store_error()?
101                    .map(|result| Ok(result.into_store_error()?).and_then(parse_transaction))
102                    .collect::<Result<Vec<TransactionRecord>, _>>()
103            },
104        }
105    }
106
107    /// Inserts a transaction and updates the current state based on the `tx_result` changes.
108    pub fn apply_transaction(
109        conn: &mut Connection,
110        smt_forest: &Arc<RwLock<AccountSmtForest>>,
111        tx_update: &TransactionStoreUpdate,
112    ) -> Result<(), StoreError> {
113        let executed_transaction = tx_update.executed_transaction();
114
115        let updated_fungible_assets = Self::get_account_fungible_assets_for_delta(
116            conn,
117            &executed_transaction.initial_account().into(),
118            executed_transaction.account_delta(),
119        )?;
120
121        let updated_storage_maps = Self::get_account_storage_maps_for_delta(
122            conn,
123            &executed_transaction.initial_account().into(),
124            executed_transaction.account_delta(),
125        )?;
126
127        let tx = conn.transaction().into_store_error()?;
128
129        // Build transaction record
130        let nullifiers: Vec<Word> = executed_transaction
131            .input_notes()
132            .iter()
133            .map(|x| x.nullifier().as_word())
134            .collect();
135
136        let output_notes = executed_transaction.output_notes();
137
138        let details = TransactionDetails {
139            account_id: executed_transaction.account_id(),
140            init_account_state: executed_transaction.initial_account().commitment(),
141            final_account_state: executed_transaction.final_account().commitment(),
142            input_note_nullifiers: nullifiers,
143            output_notes: output_notes.clone(),
144            block_num: executed_transaction.block_header().block_num(),
145            submission_height: tx_update.submission_height(),
146            expiration_block_num: executed_transaction.expiration_block_num(),
147            creation_timestamp: super::current_timestamp_u64(),
148        };
149
150        let transaction_record = TransactionRecord::new(
151            executed_transaction.id(),
152            details,
153            executed_transaction.tx_args().tx_script().cloned(),
154            TransactionStatus::Pending,
155        );
156
157        // Insert transaction data
158        upsert_transaction_record(&tx, &transaction_record)?;
159
160        // Account Data
161        let mut smt_forest = smt_forest.write().expect("smt_forest write lock not poisoned");
162        Self::apply_account_delta(
163            &tx,
164            &mut smt_forest,
165            &executed_transaction.initial_account().into(),
166            executed_transaction.final_account(),
167            updated_fungible_assets,
168            updated_storage_maps,
169            executed_transaction.account_delta(),
170        )?;
171        drop(smt_forest);
172
173        // Note Updates
174        apply_note_updates_tx(&tx, tx_update.note_updates())?;
175
176        // Note tags
177        for tag_record in tx_update.new_tags() {
178            add_note_tag_tx(&tx, tag_record)?;
179        }
180
181        tx.commit().into_store_error()?;
182
183        Ok(())
184    }
185}
186
187/// Updates the transaction record in the database, inserting it if it doesn't exist.
188pub(crate) fn upsert_transaction_record(
189    tx: &Transaction<'_>,
190    transaction: &TransactionRecord,
191) -> Result<(), StoreError> {
192    let SerializedTransactionData {
193        id,
194        script_root,
195        tx_script,
196        details,
197        block_num,
198        status_variant,
199        status,
200    } = serialize_transaction_data(transaction);
201
202    if let Some(root) = script_root.clone() {
203        tx.execute(INSERT_TRANSACTION_SCRIPT_QUERY, params![root, tx_script])
204            .into_store_error()?;
205    }
206
207    tx.execute(
208        UPSERT_TRANSACTION_QUERY,
209        params![id, details, script_root, block_num, status_variant, status],
210    )
211    .into_store_error()?;
212
213    Ok(())
214}
215
216/// Serializes the transaction record into a format suitable for storage in the database.
217fn serialize_transaction_data(transaction_record: &TransactionRecord) -> SerializedTransactionData {
218    let transaction_id: String = transaction_record.id.to_hex();
219
220    let script_root = transaction_record.script.as_ref().map(|script| script.root().to_bytes());
221    let tx_script = transaction_record.script.as_ref().map(TransactionScript::to_bytes);
222
223    SerializedTransactionData {
224        id: transaction_id,
225        script_root,
226        tx_script,
227        details: transaction_record.details.to_bytes(),
228        block_num: transaction_record.details.block_num.as_u32(),
229        status_variant: transaction_record.status.variant() as u8,
230        status: transaction_record.status.to_bytes(),
231    }
232}
233
234fn parse_transaction_columns(
235    row: &rusqlite::Row<'_>,
236) -> Result<SerializedTransactionParts, rusqlite::Error> {
237    let id: String = row.get(0)?;
238    let tx_script: Option<Vec<u8>> = row.get(1)?;
239    let details: Vec<u8> = row.get(2)?;
240    let status: Vec<u8> = row.get(3)?;
241
242    Ok(SerializedTransactionParts { id, tx_script, details, status })
243}
244
245/// Parse a transaction from the provided parts.
246fn parse_transaction(
247    serialized_transaction: SerializedTransactionParts,
248) -> Result<TransactionRecord, StoreError> {
249    let SerializedTransactionParts { id, tx_script, details, status } = serialized_transaction;
250
251    let id: Word = id.as_str().try_into()?;
252
253    let script: Option<TransactionScript> = tx_script
254        .map(|script| TransactionScript::read_from_bytes(&script))
255        .transpose()?;
256
257    Ok(TransactionRecord {
258        id: TransactionId::from_raw(id),
259        details: TransactionDetails::read_from_bytes(&details)?,
260        script,
261        status: TransactionStatus::read_from_bytes(&status)?,
262    })
263}