miden_client/note/
note_screener.rs1use alloc::boxed::Box;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4use core::fmt;
5
6use miden_lib::account::interface::AccountInterface;
7use miden_lib::note::well_known_note::WellKnownNote;
8use miden_objects::account::{Account, AccountId};
9use miden_objects::note::{Note, NoteId};
10use miden_objects::transaction::{InputNote, InputNotes};
11use miden_objects::{AccountError, AssetError};
12use miden_tx::auth::TransactionAuthenticator;
13use miden_tx::{NoteCheckerError, NoteConsumptionChecker, TransactionExecutor};
14use thiserror::Error;
15use tonic::async_trait;
16
17use crate::ClientError;
18use crate::rpc::domain::note::CommittedNote;
19use crate::store::data_store::ClientDataStore;
20use crate::store::{InputNoteRecord, NoteFilter, Store, StoreError};
21use crate::sync::{NoteUpdateAction, OnNoteReceived};
22use crate::transaction::{TransactionRequestBuilder, TransactionRequestError};
23
24#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
26pub enum NoteRelevance {
27 Now,
29 After(u32),
31}
32
33pub type NoteConsumability = (AccountId, NoteRelevance);
38
39impl fmt::Display for NoteRelevance {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 NoteRelevance::Now => write!(f, "Now"),
43 NoteRelevance::After(height) => write!(f, "After block {height}"),
44 }
45 }
46}
47
48pub struct NoteScreener<AUTH> {
55 store: Arc<dyn Store>,
57 authenticator: Option<Arc<AUTH>>,
59}
60
61impl<AUTH> NoteScreener<AUTH>
62where
63 AUTH: TransactionAuthenticator + Sync,
64{
65 pub fn new(store: Arc<dyn Store>, authenticator: Option<Arc<AUTH>>) -> Self {
66 Self { store, authenticator }
67 }
68
69 pub async fn check_relevance(
78 &self,
79 note: &Note,
80 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
81 let mut note_relevances = vec![];
82 for id in self.store.get_account_ids().await? {
83 let account_record = self
84 .store
85 .get_account(id)
86 .await?
87 .ok_or(NoteScreenerError::AccountDataNotFound(id))?;
88
89 match self.check_standard_consumability(account_record.account(), note).await {
90 Ok(Some(relevance)) => {
91 note_relevances.push((id, relevance));
92 },
93 Ok(None) => {
94 let script_root = note.script().root();
97
98 if script_root == WellKnownNote::P2IDE.script_root()
99 && let Some(relevance) = Self::check_p2ide_recall_consumability(note, &id)?
100 {
101 note_relevances.push((id, relevance));
102 }
103 },
104 Err(_) => {},
107 }
108 }
109
110 Ok(note_relevances)
111 }
112
113 async fn check_standard_consumability(
116 &self,
117 account: &Account,
118 note: &Note,
119 ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
120 let transaction_request =
121 TransactionRequestBuilder::new().build_consume_notes(vec![note.id()])?;
122
123 let tx_script = transaction_request.build_transaction_script(
124 &AccountInterface::from(account),
125 crate::DebugMode::Enabled,
126 )?;
127
128 let tx_args = transaction_request.clone().into_transaction_args(tx_script, vec![]);
129 let input_notes = InputNotes::new(vec![InputNote::unauthenticated(note.clone())])
130 .expect("Single note should be valid");
131
132 let data_store = ClientDataStore::new(self.store.clone());
133 let mut transaction_executor = TransactionExecutor::new(&data_store);
134 if let Some(authenticator) = &self.authenticator {
135 transaction_executor = transaction_executor.with_authenticator(authenticator.as_ref());
136 }
137
138 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
139
140 data_store.mast_store().load_account_code(account.code());
141 let note_execution_check = consumption_checker
142 .check_notes_consumability(
143 account.id(),
144 self.store.get_sync_height().await?,
145 input_notes,
146 tx_args,
147 )
148 .await?;
149 if !note_execution_check.successful.is_empty() {
150 return Ok(Some(NoteRelevance::Now));
151 }
152
153 Ok(None)
154 }
155
156 fn check_p2ide_recall_consumability(
159 note: &Note,
160 account_id: &AccountId,
161 ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
162 let note_inputs = note.inputs().values();
163 if note_inputs.len() != 4 {
164 return Err(InvalidNoteInputsError::WrongNumInputs(note.id(), 4).into());
165 }
166
167 let recall_height_felt = note_inputs[2];
168
169 let sender = note.metadata().sender();
170 let recall_height: u32 = recall_height_felt.as_int().try_into().map_err(|_err| {
171 InvalidNoteInputsError::BlockNumberError(note.id(), recall_height_felt.as_int())
172 })?;
173
174 if sender == *account_id {
175 Ok(Some(NoteRelevance::After(recall_height)))
176 } else {
177 Ok(None)
178 }
179 }
180}
181
182#[async_trait(?Send)]
186impl<AUTH> OnNoteReceived for NoteScreener<AUTH>
187where
188 AUTH: TransactionAuthenticator + Sync,
189{
190 async fn on_note_received(
195 &self,
196 committed_note: CommittedNote,
197 public_note: Option<InputNoteRecord>,
198 ) -> Result<NoteUpdateAction, ClientError> {
199 let note_id = *committed_note.note_id();
200
201 let input_note_present =
202 !self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.is_empty();
203 let output_note_present =
204 !self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.is_empty();
205
206 if input_note_present || output_note_present {
207 return Ok(NoteUpdateAction::Commit(committed_note));
209 }
210
211 match public_note {
212 Some(public_note) => {
213 if let Some(metadata) = public_note.metadata()
215 && self.store.get_unique_note_tags().await?.contains(&metadata.tag())
216 {
217 return Ok(NoteUpdateAction::Insert(public_note));
218 }
219
220 let new_note_relevance = self
222 .check_relevance(
223 &public_note
224 .clone()
225 .try_into()
226 .map_err(ClientError::NoteRecordConversionError)?,
227 )
228 .await?;
229 let is_relevant = !new_note_relevance.is_empty();
230 if is_relevant {
231 Ok(NoteUpdateAction::Insert(public_note))
232 } else {
233 Ok(NoteUpdateAction::Discard)
234 }
235 },
236 None => {
237 Ok(NoteUpdateAction::Discard)
240 },
241 }
242 }
243}
244
245#[derive(Debug, Error)]
250pub enum NoteScreenerError {
251 #[error("error while processing note inputs")]
252 InvalidNoteInputsError(#[from] InvalidNoteInputsError),
253 #[error("account data wasn't found for account id {0}")]
254 AccountDataNotFound(AccountId),
255 #[error("error while fetching data from the store")]
256 StoreError(#[from] StoreError),
257 #[error("error while checking note")]
258 NoteCheckerError(#[from] NoteCheckerError),
259 #[error("error while building transaction request")]
260 TransactionRequestError(#[from] TransactionRequestError),
261}
262
263#[derive(Debug, Error)]
264pub enum InvalidNoteInputsError {
265 #[error("account error for note with id {0}: {1}")]
266 AccountError(NoteId, AccountError),
267 #[error("asset error for note with id {0}: {1}")]
268 AssetError(NoteId, AssetError),
269 #[error("expected {1} note inputs for note with id {0}")]
270 WrongNumInputs(NoteId, usize),
271 #[error("note input representing block with value {1} for note with id {0}")]
272 BlockNumberError(NoteId, u64),
273}