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