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(
92 &self,
93 note: &Note,
94 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
95 Ok(self
96 .get_batch_consumability(core::slice::from_ref(note))
97 .await?
98 .remove(¬e.id())
99 .unwrap_or_default())
100 }
101
102 pub async fn get_batch_consumability(
108 &self,
109 notes: &[Note],
110 ) -> Result<BTreeMap<NoteId, Vec<NoteConsumability>>, NoteScreenerError> {
111 let account_ids = self.store.get_account_ids().await?;
112 self.screen_notes(notes, account_ids).await
113 }
114
115 pub async fn get_batch_consumability_for_account(
123 &self,
124 account_id: AccountId,
125 notes: &[Note],
126 ) -> Result<BTreeMap<NoteId, Vec<NoteConsumability>>, NoteScreenerError> {
127 self.screen_notes(notes, vec![account_id]).await
128 }
129
130 async fn screen_notes(
133 &self,
134 notes: &[Note],
135 account_ids: Vec<AccountId>,
136 ) -> Result<BTreeMap<NoteId, Vec<NoteConsumability>>, NoteScreenerError> {
137 if notes.is_empty() || account_ids.is_empty() {
138 return Ok(BTreeMap::new());
139 }
140
141 let block_ref = self.store.get_sync_height().await?;
142 let mut relevant_notes: BTreeMap<NoteId, Vec<NoteConsumability>> = BTreeMap::new();
143 let tx_args = self.tx_args();
144
145 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone())
146 .with_execution_input_cache();
147 let transaction_executor: TransactionExecutor<'_, '_, _, ()> =
153 TransactionExecutor::new(&data_store);
154 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
155
156 for account_id in account_ids {
157 let account_code = self.get_account_code(account_id).await?;
158 data_store.mast_store().load_account_code(&account_code);
159
160 let account_tx_args = self
161 .with_native_fee_conversion_info(
162 tx_args.clone(),
163 account_id,
164 &account_code,
165 block_ref,
166 )
167 .await?;
168
169 for note in notes {
170 let consumption_status = consumption_checker
171 .can_consume(
172 account_id,
173 block_ref,
174 InputNote::unauthenticated(note.clone()),
175 account_tx_args.clone(),
176 )
177 .await?;
178
179 if is_relevant(&consumption_status) {
180 relevant_notes
181 .entry(note.id())
182 .or_default()
183 .push((account_id, consumption_status));
184 }
185 }
186 }
187
188 Ok(relevant_notes)
189 }
190
191 pub async fn check_notes_consumability(
198 &self,
199 account_id: AccountId,
200 notes: Vec<Note>,
201 ) -> Result<NoteConsumptionInfo, NoteScreenerError> {
202 let block_ref = self.store.get_sync_height().await?;
203 let account_code = self.get_account_code(account_id).await?;
204 let tx_args = self
205 .with_native_fee_conversion_info(self.tx_args(), account_id, &account_code, block_ref)
206 .await?;
207
208 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone())
209 .with_execution_input_cache();
210 let transaction_executor: TransactionExecutor<'_, '_, _, ()> =
211 TransactionExecutor::new(&data_store);
212
213 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
214
215 data_store.mast_store().load_account_code(&account_code);
216 let note_consumption_info = consumption_checker
217 .check_notes_consumability(account_id, block_ref, notes, tx_args)
218 .await?;
219
220 Ok(note_consumption_info)
221 }
222
223 async fn with_native_fee_conversion_info(
236 &self,
237 tx_args: TransactionArgs,
238 account_id: AccountId,
239 account_code: &AccountCode,
240 block_ref: BlockNumber,
241 ) -> Result<TransactionArgs, NoteScreenerError> {
242 if tx_args.auth_args() != Word::empty() {
244 return Ok(tx_args);
245 }
246
247 let Some((header, _)) = self.store.get_block_header_by_num(block_ref).await? else {
249 return Ok(tx_args);
250 };
251
252 let Some(conversion_info) = native_fee_conversion_info(
253 &account_code.interface(account_id),
254 header.fee_parameters(),
255 &crate::protocol_config::load_protocol_config(
256 self.store.as_ref(),
257 header.protocol_config_commitment(),
258 )
259 .await?,
260 ) else {
261 return Ok(tx_args);
262 };
263
264 let (auth_arg, preimage) =
265 commit_fee_conversion_info(conversion_info, NATIVE_FEE_CONVERSION_SALT);
266 let mut tx_args = tx_args.with_auth_args(auth_arg);
267 tx_args.extend_advice_map([(auth_arg, preimage)]);
268
269 Ok(tx_args)
270 }
271
272 async fn get_account_code(
273 &self,
274 account_id: AccountId,
275 ) -> Result<AccountCode, NoteScreenerError> {
276 self.store
277 .get_account_code(account_id)
278 .await?
279 .ok_or(NoteScreenerError::AccountDataNotFound(account_id))
280 }
281}
282
283#[async_trait(?Send)]
287impl OnNoteReceived for NoteScreener {
288 async fn on_note_received(
293 &self,
294 committed_note: CommittedNote,
295 public_note: Option<InputNoteRecord>,
296 ) -> Result<NoteUpdateAction, ClientError> {
297 let note_id = *committed_note.note_id();
298
299 let mut input_note_present =
300 !self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.is_empty();
301
302 if !input_note_present {
307 input_note_present = self
308 .store
309 .get_input_notes(NoteFilter::Expected)
310 .await?
311 .iter()
312 .filter(|note| note.metadata().is_none())
313 .any(|note| {
314 NoteId::new(note.details_commitment(), committed_note.metadata()) == note_id
315 });
316 }
317
318 let output_note_present =
319 !self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.is_empty();
320
321 if input_note_present || output_note_present {
322 return Ok(NoteUpdateAction::Commit(committed_note));
324 }
325
326 match public_note {
327 Some(public_note) => {
328 if let Some(metadata) = public_note.metadata()
330 && self.store.get_unique_note_tags().await?.contains(&metadata.tag())
331 {
332 return Ok(NoteUpdateAction::Insert(public_note));
333 }
334
335 let new_note_relevance = self
337 .get_consumability(
338 &public_note
339 .clone()
340 .try_into()
341 .map_err(ClientError::NoteRecordConversionError)?,
342 )
343 .await?;
344 let is_relevant = !new_note_relevance.is_empty();
345 if is_relevant {
346 Ok(NoteUpdateAction::Insert(public_note))
347 } else {
348 Ok(NoteUpdateAction::Discard)
349 }
350 },
351 None => {
352 Ok(NoteUpdateAction::Discard)
355 },
356 }
357 }
358}
359
360#[derive(Debug, Error)]
365pub enum NoteScreenerError {
366 #[error("account {0} data not found in the store")]
367 AccountDataNotFound(AccountId),
368 #[error("failed to fetch data from the store")]
369 StoreError(#[from] StoreError),
370 #[error("note consumption check failed")]
371 NoteCheckerError(#[from] NoteCheckerError),
372 #[error("failed to build transaction request")]
373 TransactionRequestError(#[from] TransactionRequestError),
374}