miden_client/note/
note_screener.rs1use alloc::boxed::Box;
2use alloc::collections::BTreeMap;
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use async_trait::async_trait;
7use miden_protocol::account::{AccountCode, AccountId};
8use miden_protocol::note::{Note, NoteId};
9use miden_standards::note::NoteConsumptionStatus;
10use miden_tx::{
11 NoteCheckerError,
12 NoteConsumptionChecker,
13 NoteConsumptionInfo,
14 TransactionExecutor,
15};
16use thiserror::Error;
17
18use crate::ClientError;
19use crate::rpc::NodeRpcClient;
20use crate::rpc::domain::note::CommittedNote;
21use crate::store::data_store::ClientDataStore;
22use crate::store::{InputNoteRecord, NoteFilter, Store, StoreError};
23use crate::sync::{NoteUpdateAction, OnNoteReceived};
24use crate::transaction::{AdviceMap, InputNote, TransactionArgs, TransactionRequestError};
25
26pub type NoteConsumability = (AccountId, NoteConsumptionStatus);
31
32fn is_relevant(consumption_status: &NoteConsumptionStatus) -> bool {
36 !matches!(
37 consumption_status,
38 NoteConsumptionStatus::NeverConsumable(_) | NoteConsumptionStatus::UnconsumableConditions
39 )
40}
41
42#[derive(Clone)]
49pub struct NoteScreener {
50 store: Arc<dyn Store>,
52 tx_args: Option<TransactionArgs>,
54 rpc_api: Arc<dyn NodeRpcClient>,
56}
57
58impl NoteScreener {
59 pub fn new(store: Arc<dyn Store>, rpc_api: Arc<dyn NodeRpcClient>) -> Self {
60 Self { store, tx_args: None, rpc_api }
61 }
62
63 #[must_use]
66 pub fn with_transaction_args(mut self, tx_args: TransactionArgs) -> Self {
67 self.tx_args = Some(tx_args);
68 self
69 }
70
71 fn tx_args(&self) -> TransactionArgs {
72 self.tx_args
73 .clone()
74 .unwrap_or_else(|| TransactionArgs::new(AdviceMap::default()))
75 }
76
77 pub async fn get_consumability(
83 &self,
84 note: &Note,
85 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
86 Ok(self
87 .get_batch_consumability(core::slice::from_ref(note))
88 .await?
89 .remove(¬e.id())
90 .unwrap_or_default())
91 }
92
93 pub async fn get_batch_consumability(
99 &self,
100 notes: &[Note],
101 ) -> Result<BTreeMap<NoteId, Vec<NoteConsumability>>, NoteScreenerError> {
102 let account_ids = self.store.get_account_ids().await?;
103 self.screen_notes(notes, account_ids).await
104 }
105
106 pub async fn get_batch_consumability_for_account(
114 &self,
115 account_id: AccountId,
116 notes: &[Note],
117 ) -> Result<BTreeMap<NoteId, Vec<NoteConsumability>>, NoteScreenerError> {
118 self.screen_notes(notes, vec![account_id]).await
119 }
120
121 async fn screen_notes(
124 &self,
125 notes: &[Note],
126 account_ids: Vec<AccountId>,
127 ) -> Result<BTreeMap<NoteId, Vec<NoteConsumability>>, NoteScreenerError> {
128 if notes.is_empty() || account_ids.is_empty() {
129 return Ok(BTreeMap::new());
130 }
131
132 let block_ref = self.store.get_sync_height().await?;
133 let mut relevant_notes: BTreeMap<NoteId, Vec<NoteConsumability>> = BTreeMap::new();
134 let tx_args = self.tx_args();
135
136 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone())
137 .with_execution_input_cache();
138 let transaction_executor: TransactionExecutor<'_, '_, _, ()> =
145 TransactionExecutor::new(&data_store);
146 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
147
148 for account_id in account_ids {
149 let account_code = self.get_account_code(account_id).await?;
150 data_store.mast_store().load_account_code(&account_code);
151
152 for note in notes {
153 let consumption_status = consumption_checker
154 .can_consume(
155 account_id,
156 block_ref,
157 InputNote::unauthenticated(note.clone()),
158 tx_args.clone(),
159 )
160 .await?;
161
162 if is_relevant(&consumption_status) {
163 relevant_notes
164 .entry(note.id())
165 .or_default()
166 .push((account_id, consumption_status));
167 }
168 }
169 }
170
171 Ok(relevant_notes)
172 }
173
174 pub async fn check_notes_consumability(
181 &self,
182 account_id: AccountId,
183 notes: Vec<Note>,
184 ) -> Result<NoteConsumptionInfo, NoteScreenerError> {
185 let block_ref = self.store.get_sync_height().await?;
186 let tx_args = self.tx_args();
187 let account_code = self.get_account_code(account_id).await?;
188
189 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone())
190 .with_execution_input_cache();
191 let transaction_executor: TransactionExecutor<'_, '_, _, ()> =
192 TransactionExecutor::new(&data_store);
193
194 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
195
196 data_store.mast_store().load_account_code(&account_code);
197 let note_consumption_info = consumption_checker
198 .check_notes_consumability(account_id, block_ref, notes, tx_args)
199 .await?;
200
201 Ok(note_consumption_info)
202 }
203
204 async fn get_account_code(
205 &self,
206 account_id: AccountId,
207 ) -> Result<AccountCode, NoteScreenerError> {
208 self.store
209 .get_account_code(account_id)
210 .await?
211 .ok_or(NoteScreenerError::AccountDataNotFound(account_id))
212 }
213}
214
215#[async_trait(?Send)]
219impl OnNoteReceived for NoteScreener {
220 async fn on_note_received(
225 &self,
226 committed_note: CommittedNote,
227 public_note: Option<InputNoteRecord>,
228 ) -> Result<NoteUpdateAction, ClientError> {
229 let note_id = *committed_note.note_id();
230
231 let mut input_note_present =
232 !self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.is_empty();
233
234 if !input_note_present {
239 input_note_present = self
240 .store
241 .get_input_notes(NoteFilter::Expected)
242 .await?
243 .iter()
244 .filter(|note| note.metadata().is_none())
245 .any(|note| {
246 NoteId::new(note.details_commitment(), committed_note.metadata()) == note_id
247 });
248 }
249
250 let output_note_present =
251 !self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.is_empty();
252
253 if input_note_present || output_note_present {
254 return Ok(NoteUpdateAction::Commit(committed_note));
256 }
257
258 match public_note {
259 Some(public_note) => {
260 if let Some(metadata) = public_note.metadata()
262 && self.store.get_unique_note_tags().await?.contains(&metadata.tag())
263 {
264 return Ok(NoteUpdateAction::Insert(public_note));
265 }
266
267 let new_note_relevance = self
269 .get_consumability(
270 &public_note
271 .clone()
272 .try_into()
273 .map_err(ClientError::NoteRecordConversionError)?,
274 )
275 .await?;
276 let is_relevant = !new_note_relevance.is_empty();
277 if is_relevant {
278 Ok(NoteUpdateAction::Insert(public_note))
279 } else {
280 Ok(NoteUpdateAction::Discard)
281 }
282 },
283 None => {
284 Ok(NoteUpdateAction::Discard)
287 },
288 }
289 }
290}
291
292#[derive(Debug, Error)]
297pub enum NoteScreenerError {
298 #[error("account {0} data not found in the store")]
299 AccountDataNotFound(AccountId),
300 #[error("failed to fetch data from the store")]
301 StoreError(#[from] StoreError),
302 #[error("note consumption check failed")]
303 NoteCheckerError(#[from] NoteCheckerError),
304 #[error("failed to build transaction request")]
305 TransactionRequestError(#[from] TransactionRequestError),
306}