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::auth::TransactionAuthenticator;
11use miden_tx::{
12 NoteCheckerError,
13 NoteConsumptionChecker,
14 NoteConsumptionInfo,
15 TransactionExecutor,
16};
17use thiserror::Error;
18
19use crate::ClientError;
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<AUTH> {
50 store: Arc<dyn Store>,
52 authenticator: Option<Arc<AUTH>>,
54 tx_args: Option<TransactionArgs>,
56}
57
58impl<AUTH> NoteScreener<AUTH>
59where
60 AUTH: TransactionAuthenticator + Sync,
61{
62 pub fn new(store: Arc<dyn Store>, authenticator: Option<Arc<AUTH>>) -> Self {
63 Self { store, authenticator, tx_args: None }
64 }
65
66 #[must_use]
69 pub fn with_transaction_args(mut self, tx_args: TransactionArgs) -> Self {
70 self.tx_args = Some(tx_args);
71 self
72 }
73
74 fn tx_args(&self) -> TransactionArgs {
75 self.tx_args
76 .clone()
77 .unwrap_or_else(|| TransactionArgs::new(AdviceMap::default()))
78 }
79
80 pub async fn can_consume(
85 &self,
86 note: &Note,
87 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
88 Ok(self
89 .can_consume_batch(core::slice::from_ref(note))
90 .await?
91 .remove(¬e.id())
92 .unwrap_or_default())
93 }
94
95 pub async fn can_consume_batch(
101 &self,
102 notes: &[Note],
103 ) -> Result<BTreeMap<NoteId, Vec<NoteConsumability>>, NoteScreenerError> {
104 let account_ids = self.store.get_account_ids().await?;
105 if notes.is_empty() || account_ids.is_empty() {
106 return Ok(BTreeMap::new());
107 }
108
109 let block_ref = self.store.get_sync_height().await?;
110 let mut relevant_notes: BTreeMap<NoteId, Vec<NoteConsumability>> = BTreeMap::new();
111 let tx_args = self.tx_args();
112
113 let data_store = ClientDataStore::new(self.store.clone());
114 let mut transaction_executor = TransactionExecutor::new(&data_store);
115 if let Some(authenticator) = &self.authenticator {
116 transaction_executor = transaction_executor.with_authenticator(authenticator.as_ref());
117 }
118 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
119
120 for account_id in account_ids {
121 let account_code = self.get_account_code(account_id).await?;
122 data_store.mast_store().load_account_code(&account_code);
123
124 for note in notes {
125 let consumption_status = consumption_checker
126 .can_consume(
127 account_id,
128 block_ref,
129 InputNote::unauthenticated(note.clone()),
130 tx_args.clone(),
131 )
132 .await?;
133
134 if is_relevant(&consumption_status) {
135 relevant_notes
136 .entry(note.id())
137 .or_default()
138 .push((account_id, consumption_status));
139 }
140 }
141 }
142
143 Ok(relevant_notes)
144 }
145
146 pub async fn check_notes_consumability(
153 &self,
154 account_id: AccountId,
155 notes: Vec<Note>,
156 ) -> Result<NoteConsumptionInfo, NoteScreenerError> {
157 let block_ref = self.store.get_sync_height().await?;
158 let tx_args = self.tx_args();
159 let account_code = self.get_account_code(account_id).await?;
160
161 let data_store = ClientDataStore::new(self.store.clone());
162 let mut transaction_executor = TransactionExecutor::new(&data_store);
163 if let Some(authenticator) = &self.authenticator {
164 transaction_executor = transaction_executor.with_authenticator(authenticator.as_ref());
165 }
166
167 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
168
169 data_store.mast_store().load_account_code(&account_code);
170 let note_consumption_info = consumption_checker
171 .check_notes_consumability(account_id, block_ref, notes, tx_args)
172 .await?;
173
174 Ok(note_consumption_info)
175 }
176
177 async fn get_account_code(
178 &self,
179 account_id: AccountId,
180 ) -> Result<AccountCode, NoteScreenerError> {
181 self.store
182 .get_account_code(account_id)
183 .await?
184 .ok_or(NoteScreenerError::AccountDataNotFound(account_id))
185 }
186}
187
188#[async_trait(?Send)]
192impl<AUTH> OnNoteReceived for NoteScreener<AUTH>
193where
194 AUTH: TransactionAuthenticator + Sync,
195{
196 async fn on_note_received(
201 &self,
202 committed_note: CommittedNote,
203 public_note: Option<InputNoteRecord>,
204 ) -> Result<NoteUpdateAction, ClientError> {
205 let note_id = *committed_note.note_id();
206
207 let input_note_present =
208 !self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.is_empty();
209 let output_note_present =
210 !self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.is_empty();
211
212 if input_note_present || output_note_present {
213 return Ok(NoteUpdateAction::Commit(committed_note));
215 }
216
217 match public_note {
218 Some(public_note) => {
219 if let Some(metadata) = public_note.metadata()
221 && self.store.get_unique_note_tags().await?.contains(&metadata.tag())
222 {
223 return Ok(NoteUpdateAction::Insert(public_note));
224 }
225
226 let new_note_relevance = self
228 .can_consume(
229 &public_note
230 .clone()
231 .try_into()
232 .map_err(ClientError::NoteRecordConversionError)?,
233 )
234 .await?;
235 let is_relevant = !new_note_relevance.is_empty();
236 if is_relevant {
237 Ok(NoteUpdateAction::Insert(public_note))
238 } else {
239 Ok(NoteUpdateAction::Discard)
240 }
241 },
242 None => {
243 Ok(NoteUpdateAction::Discard)
246 },
247 }
248 }
249}
250
251#[derive(Debug, Error)]
256pub enum NoteScreenerError {
257 #[error("account {0} data not found in the store")]
258 AccountDataNotFound(AccountId),
259 #[error("failed to fetch data from the store")]
260 StoreError(#[from] StoreError),
261 #[error("note consumption check failed")]
262 NoteCheckerError(#[from] NoteCheckerError),
263 #[error("failed to build transaction request")]
264 TransactionRequestError(#[from] TransactionRequestError),
265}