miden_client_sqlite_store/
transaction.rs1#![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
44struct SerializedTransactionData {
48 id: String,
50 script_root: Option<Vec<u8>>,
52 tx_script: Option<Vec<u8>>,
54 details: Vec<u8>,
56 block_num: u32,
58 status_variant: u8,
60 status: Vec<u8>,
62}
63
64struct SerializedTransactionParts {
65 id: String,
67 tx_script: Option<Vec<u8>>,
69 details: Vec<u8>,
71 status: Vec<u8>,
73}
74
75impl SqliteStore {
76 pub fn get_transactions(
78 conn: &mut Connection,
79 filter: &TransactionFilter,
80 ) -> Result<Vec<TransactionRecord>, StoreError> {
81 match filter {
82 TransactionFilter::Ids(ids) => {
83 let id_strings =
85 ids.iter().map(|id| Value::Text(id.to_string())).collect::<Vec<_>>();
86
87 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 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 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 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 upsert_transaction_record(&tx, &transaction_record)?;
159
160 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 apply_note_updates_tx(&tx, tx_update.note_updates())?;
175
176 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
187pub(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
216fn 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
245fn 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}