1use alloc::boxed::Box;
2use alloc::collections::BTreeMap;
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use async_trait::async_trait;
7use miden_protocol::Word;
8use miden_protocol::account::{AccountCode, AccountId};
9use miden_protocol::block::BlockNumber;
10use miden_protocol::note::{Note, NoteId};
11use miden_standards::account::auth::commit_fee_conversion_info;
12use miden_standards::note::NoteConsumptionStatus;
13use miden_tx::{
14 NoteCheckerError,
15 NoteConsumptionChecker,
16 NoteConsumptionInfo,
17 TransactionExecutor,
18};
19use thiserror::Error;
20
21use crate::ClientError;
22use crate::rpc::NodeRpcClient;
23use crate::rpc::domain::note::CommittedNote;
24use crate::store::data_store::ClientDataStore;
25use crate::store::{InputNoteRecord, NoteFilter, Store, StoreError};
26use crate::sync::{NoteUpdateAction, OnNoteReceived};
27use crate::transaction::{
28 AdviceMap,
29 InputNote,
30 NATIVE_FEE_CONVERSION_SALT,
31 TransactionArgs,
32 TransactionRequestError,
33 native_fee_conversion_info,
34};
35
36pub type NoteConsumability = (AccountId, NoteConsumptionStatus);
41
42fn is_relevant(consumption_status: &NoteConsumptionStatus) -> bool {
46 !matches!(
47 consumption_status,
48 NoteConsumptionStatus::NeverConsumable(_) | NoteConsumptionStatus::UnconsumableConditions
49 )
50}
51
52#[derive(Clone)]
59pub struct NoteScreener {
60 store: Arc<dyn Store>,
62 tx_args: Option<TransactionArgs>,
64 rpc_api: Arc<dyn NodeRpcClient>,
66}
67
68impl NoteScreener {
69 pub fn new(store: Arc<dyn Store>, rpc_api: Arc<dyn NodeRpcClient>) -> Self {
70 Self { store, tx_args: None, rpc_api }
71 }
72
73 #[must_use]
76 pub fn with_transaction_args(mut self, tx_args: TransactionArgs) -> Self {
77 self.tx_args = Some(tx_args);
78 self
79 }
80
81 fn tx_args(&self) -> TransactionArgs {
82 self.tx_args
83 .clone()
84 .unwrap_or_else(|| TransactionArgs::new(AdviceMap::default()))
85 }
86
87 pub async fn get_consumability(
93 &self,
94 note: &Note,
95 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
96 Ok(self
97 .get_batch_consumability(core::slice::from_ref(note))
98 .await?
99 .remove(¬e.id())
100 .unwrap_or_default())
101 }
102
103 pub async fn get_batch_consumability(
109 &self,
110 notes: &[Note],
111 ) -> Result<BTreeMap<NoteId, Vec<NoteConsumability>>, NoteScreenerError> {
112 let account_ids = self.store.get_account_ids().await?;
113 self.screen_notes(notes, account_ids).await
114 }
115
116 pub async fn get_batch_consumability_for_account(
124 &self,
125 account_id: AccountId,
126 notes: &[Note],
127 ) -> Result<BTreeMap<NoteId, Vec<NoteConsumability>>, NoteScreenerError> {
128 self.screen_notes(notes, vec![account_id]).await
129 }
130
131 async fn screen_notes(
134 &self,
135 notes: &[Note],
136 account_ids: Vec<AccountId>,
137 ) -> Result<BTreeMap<NoteId, Vec<NoteConsumability>>, NoteScreenerError> {
138 if notes.is_empty() || account_ids.is_empty() {
139 return Ok(BTreeMap::new());
140 }
141
142 let block_ref = self.store.get_sync_height().await?;
143 let mut relevant_notes: BTreeMap<NoteId, Vec<NoteConsumability>> = BTreeMap::new();
144 let tx_args = self.tx_args();
145
146 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone())
147 .with_execution_input_cache();
148 let transaction_executor: TransactionExecutor<'_, '_, _, ()> =
155 TransactionExecutor::new(&data_store);
156 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
157
158 for account_id in account_ids {
159 let account_code = self.get_account_code(account_id).await?;
160 data_store.mast_store().load_account_code(&account_code);
161
162 let account_tx_args = self
163 .with_native_fee_conversion_info(
164 tx_args.clone(),
165 account_id,
166 &account_code,
167 block_ref,
168 )
169 .await?;
170
171 for note in notes {
172 let consumption_status = consumption_checker
173 .can_consume(
174 account_id,
175 block_ref,
176 InputNote::unauthenticated(note.clone()),
177 account_tx_args.clone(),
178 )
179 .await?;
180
181 if is_relevant(&consumption_status) {
182 relevant_notes
183 .entry(note.id())
184 .or_default()
185 .push((account_id, consumption_status));
186 }
187 }
188 }
189
190 Ok(relevant_notes)
191 }
192
193 pub async fn check_notes_consumability(
200 &self,
201 account_id: AccountId,
202 notes: Vec<Note>,
203 ) -> Result<NoteConsumptionInfo, NoteScreenerError> {
204 let block_ref = self.store.get_sync_height().await?;
205 let account_code = self.get_account_code(account_id).await?;
206 let tx_args = self
207 .with_native_fee_conversion_info(self.tx_args(), account_id, &account_code, block_ref)
208 .await?;
209
210 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone())
211 .with_execution_input_cache();
212 let transaction_executor: TransactionExecutor<'_, '_, _, ()> =
213 TransactionExecutor::new(&data_store);
214
215 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
216
217 data_store.mast_store().load_account_code(&account_code);
218 let note_consumption_info = consumption_checker
219 .check_notes_consumability(account_id, block_ref, notes, tx_args)
220 .await?;
221
222 Ok(note_consumption_info)
223 }
224
225 async fn with_native_fee_conversion_info(
238 &self,
239 tx_args: TransactionArgs,
240 account_id: AccountId,
241 account_code: &AccountCode,
242 block_ref: BlockNumber,
243 ) -> Result<TransactionArgs, NoteScreenerError> {
244 if tx_args.auth_args() != Word::empty() {
246 return Ok(tx_args);
247 }
248
249 let Some((header, _)) = self.store.get_block_header_by_num(block_ref).await? else {
251 return Ok(tx_args);
252 };
253
254 let Some(conversion_info) = native_fee_conversion_info(
255 &account_code.interface(account_id),
256 header.fee_parameters(),
257 ) else {
258 return Ok(tx_args);
259 };
260
261 let (auth_arg, preimage) =
262 commit_fee_conversion_info(conversion_info, NATIVE_FEE_CONVERSION_SALT);
263 let mut tx_args = tx_args.with_auth_args(auth_arg);
264 tx_args.extend_advice_map([(auth_arg, preimage)]);
265
266 Ok(tx_args)
267 }
268
269 async fn get_account_code(
270 &self,
271 account_id: AccountId,
272 ) -> Result<AccountCode, NoteScreenerError> {
273 self.store
274 .get_account_code(account_id)
275 .await?
276 .ok_or(NoteScreenerError::AccountDataNotFound(account_id))
277 }
278}
279
280#[async_trait(?Send)]
284impl OnNoteReceived for NoteScreener {
285 async fn on_note_received(
290 &self,
291 committed_note: CommittedNote,
292 public_note: Option<InputNoteRecord>,
293 ) -> Result<NoteUpdateAction, ClientError> {
294 let note_id = *committed_note.note_id();
295
296 let mut input_note_present =
297 !self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.is_empty();
298
299 if !input_note_present {
304 input_note_present = self
305 .store
306 .get_input_notes(NoteFilter::Expected)
307 .await?
308 .iter()
309 .filter(|note| note.metadata().is_none())
310 .any(|note| {
311 NoteId::new(note.details_commitment(), committed_note.metadata()) == note_id
312 });
313 }
314
315 let output_note_present =
316 !self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.is_empty();
317
318 if input_note_present || output_note_present {
319 return Ok(NoteUpdateAction::Commit(committed_note));
321 }
322
323 match public_note {
324 Some(public_note) => {
325 if let Some(metadata) = public_note.metadata()
327 && self.store.get_unique_note_tags().await?.contains(&metadata.tag())
328 {
329 return Ok(NoteUpdateAction::Insert(public_note));
330 }
331
332 let new_note_relevance = self
334 .get_consumability(
335 &public_note
336 .clone()
337 .try_into()
338 .map_err(ClientError::NoteRecordConversionError)?,
339 )
340 .await?;
341 let is_relevant = !new_note_relevance.is_empty();
342 if is_relevant {
343 Ok(NoteUpdateAction::Insert(public_note))
344 } else {
345 Ok(NoteUpdateAction::Discard)
346 }
347 },
348 None => {
349 Ok(NoteUpdateAction::Discard)
352 },
353 }
354 }
355}
356
357#[derive(Debug, Error)]
362pub enum NoteScreenerError {
363 #[error("account {0} data not found in the store")]
364 AccountDataNotFound(AccountId),
365 #[error("failed to fetch data from the store")]
366 StoreError(#[from] StoreError),
367 #[error("note consumption check failed")]
368 NoteCheckerError(#[from] NoteCheckerError),
369 #[error("failed to build transaction request")]
370 TransactionRequestError(#[from] TransactionRequestError),
371}