miden_client_sqlite_store/
transaction.rs1#![allow(clippy::items_after_statements)]
2
3use std::rc::Rc;
4use std::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
43struct SerializedTransactionData {
47 id: Vec<u8>,
49 script_root: Option<Vec<u8>>,
51 tx_script: Option<Vec<u8>>,
53 details: Vec<u8>,
55 block_num: u32,
57 status_variant: u8,
59 status: Vec<u8>,
61}
62
63struct SerializedTransactionParts {
64 id: Vec<u8>,
66 tx_script: Option<Vec<u8>>,
68 details: Vec<u8>,
70 status: Vec<u8>,
72}
73
74impl SqliteStore {
75 pub fn get_transactions(
77 conn: &mut Connection,
78 filter: &TransactionFilter,
79 ) -> Result<Vec<TransactionRecord>, StoreError> {
80 match filter {
81 TransactionFilter::Ids(ids) => {
82 let id_blobs = ids.iter().map(|id| Value::Blob(id.to_bytes())).collect::<Vec<_>>();
83
84 conn.prepare(filter.to_query().as_ref())
86 .into_store_error()?
87 .query_map(params![Rc::new(id_blobs)], parse_transaction_columns)
88 .into_store_error()?
89 .map(|result| Ok(result.into_store_error()?).and_then(parse_transaction))
90 .collect::<Result<Vec<TransactionRecord>, _>>()
91 },
92 _ => {
93 conn.prepare(filter.to_query().as_ref())
95 .into_store_error()?
96 .query_map([], parse_transaction_columns)
97 .into_store_error()?
98 .map(|result| Ok(result.into_store_error()?).and_then(parse_transaction))
99 .collect::<Result<Vec<TransactionRecord>, _>>()
100 },
101 }
102 }
103
104 pub fn apply_transaction(
110 conn: &mut Connection,
111 smt_forest: &Arc<RwLock<AccountSmtForest>>,
112 tx_update: &TransactionStoreUpdate,
113 ) -> Result<(), StoreError> {
114 with_forest_snapshot(conn, smt_forest, |db_tx, forest| {
115 Self::apply_transaction_in_txn(db_tx, forest, tx_update)
116 })
117 }
118
119 pub fn apply_transaction_batch(
124 conn: &mut Connection,
125 smt_forest: &Arc<RwLock<AccountSmtForest>>,
126 tx_updates: &[TransactionStoreUpdate],
127 ) -> Result<(), StoreError> {
128 with_forest_snapshot(conn, smt_forest, |db_tx, forest| {
129 for update in tx_updates {
130 Self::apply_transaction_in_txn(db_tx, forest, update)?;
131 }
132 Ok(())
133 })
134 }
135
136 pub(crate) fn apply_transaction_in_txn(
147 db_tx: &mut Transaction<'_>,
148 smt_forest: &mut AccountSmtForest,
149 tx_update: &TransactionStoreUpdate,
150 ) -> Result<(), StoreError> {
151 let executed_transaction = tx_update.executed_transaction();
152 let account_patch = executed_transaction.account_patch();
153
154 let old_map_roots = Self::get_storage_map_roots_for_patch(
155 db_tx,
156 executed_transaction.account_id(),
157 account_patch.storage(),
158 )?;
159
160 let nullifiers: Vec<Word> = executed_transaction
162 .input_notes()
163 .iter()
164 .map(|x| x.nullifier().as_word())
165 .collect();
166
167 let output_notes = executed_transaction.output_notes();
168
169 let details = TransactionDetails {
170 account_id: executed_transaction.account_id(),
171 init_account_state: executed_transaction.initial_account().initial_commitment(),
172 final_account_state: executed_transaction.final_account().to_commitment(),
173 input_note_nullifiers: nullifiers,
174 output_notes: output_notes.clone(),
175 block_num: executed_transaction.block_header().block_num(),
176 submission_height: tx_update.submission_height(),
177 expiration_block_num: executed_transaction.expiration_block_num(),
178 creation_timestamp: super::current_timestamp_u64(),
179 };
180
181 let transaction_record = TransactionRecord::new(
182 executed_transaction.id(),
183 details,
184 executed_transaction.tx_args().tx_script().cloned(),
185 TransactionStatus::Pending,
186 );
187
188 upsert_transaction_record(db_tx, &transaction_record)?;
190
191 Self::apply_account_patch(
193 db_tx,
194 smt_forest,
195 &executed_transaction.initial_account().into(),
196 executed_transaction.final_account(),
197 &old_map_roots,
198 account_patch,
199 )?;
200
201 apply_note_updates_tx(db_tx, tx_update.note_updates())?;
203
204 for tag_record in tx_update.new_tags() {
206 add_note_tag_tx(db_tx, tag_record)?;
207 }
208
209 Ok(())
210 }
211}
212
213pub(crate) fn with_forest_snapshot<F, T>(
222 conn: &mut Connection,
223 smt_forest: &Arc<RwLock<AccountSmtForest>>,
224 f: F,
225) -> Result<T, StoreError>
226where
227 F: FnOnce(&mut Transaction<'_>, &mut AccountSmtForest) -> Result<T, StoreError>,
228{
229 let mut db_tx = conn.transaction().into_store_error()?;
230 let mut forest = smt_forest
231 .write()
232 .map_err(|_| StoreError::DatabaseError("smt_forest write lock poisoned".to_string()))?;
233 let mut working_forest = forest.clone();
234
235 let value = f(&mut db_tx, &mut working_forest)?;
236 db_tx.commit().into_store_error()?;
237
238 *forest = working_forest;
239 Ok(value)
240}
241
242pub(crate) fn upsert_transaction_record(
244 tx: &Transaction<'_>,
245 transaction: &TransactionRecord,
246) -> Result<(), StoreError> {
247 let SerializedTransactionData {
248 id,
249 script_root,
250 tx_script,
251 details,
252 block_num,
253 status_variant,
254 status,
255 } = serialize_transaction_data(transaction);
256
257 if let Some(root) = script_root.clone() {
258 tx.execute(INSERT_TRANSACTION_SCRIPT_QUERY, params![root, tx_script])
259 .into_store_error()?;
260 }
261
262 tx.execute(
263 UPSERT_TRANSACTION_QUERY,
264 params![id, details, script_root, block_num, status_variant, status],
265 )
266 .into_store_error()?;
267
268 Ok(())
269}
270
271fn serialize_transaction_data(transaction_record: &TransactionRecord) -> SerializedTransactionData {
273 let transaction_id = transaction_record.id.to_bytes();
274
275 let script_root = transaction_record.script.as_ref().map(|script| script.root().to_bytes());
276 let tx_script = transaction_record.script.as_ref().map(TransactionScript::to_bytes);
277
278 SerializedTransactionData {
279 id: transaction_id,
280 script_root,
281 tx_script,
282 details: transaction_record.details.to_bytes(),
283 block_num: transaction_record.details.block_num.as_u32(),
284 status_variant: transaction_record.status.variant() as u8,
285 status: transaction_record.status.to_bytes(),
286 }
287}
288
289fn parse_transaction_columns(
290 row: &rusqlite::Row<'_>,
291) -> Result<SerializedTransactionParts, rusqlite::Error> {
292 let id: Vec<u8> = row.get(0)?;
293 let tx_script: Option<Vec<u8>> = row.get(1)?;
294 let details: Vec<u8> = row.get(2)?;
295 let status: Vec<u8> = row.get(3)?;
296
297 Ok(SerializedTransactionParts { id, tx_script, details, status })
298}
299
300fn parse_transaction(
302 serialized_transaction: SerializedTransactionParts,
303) -> Result<TransactionRecord, StoreError> {
304 let SerializedTransactionParts { id, tx_script, details, status } = serialized_transaction;
305
306 let id = TransactionId::read_from_bytes(&id)?;
307
308 let script: Option<TransactionScript> = tx_script
309 .map(|script| TransactionScript::read_from_bytes(&script))
310 .transpose()?;
311
312 Ok(TransactionRecord {
313 id,
314 details: TransactionDetails::read_from_bytes(&details)?,
315 script,
316 status: TransactionStatus::read_from_bytes(&status)?,
317 })
318}