miden_client_sqlite_store/
transaction.rs1#![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
42struct SerializedTransactionData {
46 id: Vec<u8>,
48 script_root: Option<Vec<u8>>,
50 tx_script: Option<Vec<u8>>,
52 details: Vec<u8>,
54 block_num: u32,
56 status_variant: u8,
58 status: Vec<u8>,
60}
61
62struct SerializedTransactionParts {
63 id: Vec<u8>,
65 tx_script: Option<Vec<u8>>,
67 details: Vec<u8>,
69 status: Vec<u8>,
71}
72
73impl SqliteStore {
74 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 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 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 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 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 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 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 upsert_transaction_record(db_tx, &transaction_record)?;
183
184 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 apply_note_updates_tx(db_tx, tx_update.note_updates())?;
195
196 for tag_record in tx_update.new_tags() {
198 add_note_tag_tx(db_tx, tag_record)?;
199 }
200
201 Ok(())
202 }
203}
204
205pub(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
234fn 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
263fn 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}