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;
11use miden_objects::{AccountError, AssetError};
12use miden_tx::auth::TransactionAuthenticator;
13use miden_tx::{
14 NoteCheckerError,
15 NoteConsumptionChecker,
16 NoteConsumptionStatus,
17 TransactionExecutor,
18};
19use thiserror::Error;
20use tonic::async_trait;
21
22use crate::ClientError;
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::{TransactionRequestBuilder, TransactionRequestError};
28
29#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
31pub enum NoteRelevance {
32 Now,
34 After(u32),
36}
37
38pub type NoteConsumability = (AccountId, NoteRelevance);
43
44impl fmt::Display for NoteRelevance {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 NoteRelevance::Now => write!(f, "Now"),
48 NoteRelevance::After(height) => write!(f, "After block {height}"),
49 }
50 }
51}
52
53pub struct NoteScreener<AUTH> {
60 store: Arc<dyn Store>,
62 authenticator: Option<Arc<AUTH>>,
64}
65
66impl<AUTH> NoteScreener<AUTH>
67where
68 AUTH: TransactionAuthenticator + Sync,
69{
70 pub fn new(store: Arc<dyn Store>, authenticator: Option<Arc<AUTH>>) -> Self {
71 Self { store, authenticator }
72 }
73
74 pub async fn check_relevance(
79 &self,
80 note: &Note,
81 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
82 let mut note_relevances = vec![];
83 for id in self.store.get_account_ids().await? {
84 let account_record = self
85 .store
86 .get_account(id)
87 .await?
88 .ok_or(NoteScreenerError::AccountDataNotFound(id))?;
89
90 match self.check_standard_consumability(account_record.account(), note).await {
91 Ok(Some(relevance)) => {
92 note_relevances.push((id, relevance));
93 },
94 Ok(None) => {
95 let script_root = note.script().root();
98
99 if script_root == WellKnownNote::P2IDE.script_root()
100 && let Some(relevance) = Self::check_p2ide_recall_consumability(note, &id)?
101 {
102 note_relevances.push((id, relevance));
103 }
104 },
105 Err(_) => {},
108 }
109 }
110
111 Ok(note_relevances)
112 }
113
114 async fn check_standard_consumability(
117 &self,
118 account: &Account,
119 note: &Note,
120 ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
121 let transaction_request =
122 TransactionRequestBuilder::new().build_consume_notes(vec![note.id()])?;
123
124 let tx_script = transaction_request.build_transaction_script(
125 &AccountInterface::from(account),
126 crate::DebugMode::Enabled,
127 )?;
128
129 let tx_args = transaction_request.clone().into_transaction_args(tx_script, vec![]);
130
131 let data_store = ClientDataStore::new(self.store.clone());
132 let mut transaction_executor = TransactionExecutor::new(&data_store);
133 if let Some(authenticator) = &self.authenticator {
134 transaction_executor = transaction_executor.with_authenticator(authenticator.as_ref());
135 }
136
137 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
138
139 data_store.mast_store().load_account_code(account.code());
140 let note_consumption_check = consumption_checker
141 .can_consume(
142 account.id(),
143 self.store.get_sync_height().await?,
144 InputNote::unauthenticated(note.clone()),
145 tx_args,
146 )
147 .await?;
148
149 let result = match note_consumption_check {
150 NoteConsumptionStatus::ConsumableAfter(block_number) => {
151 Some(NoteRelevance::After(block_number.as_u32()))
152 },
153 NoteConsumptionStatus::Consumable
154 | NoteConsumptionStatus::UnconsumableWithoutAuthorization => Some(NoteRelevance::Now),
155 NoteConsumptionStatus::Unconsumable | NoteConsumptionStatus::Incompatible => None,
159 };
160 Ok(result)
161 }
162
163 fn check_p2ide_recall_consumability(
166 note: &Note,
167 account_id: &AccountId,
168 ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
169 let note_inputs = note.inputs().values();
170 if note_inputs.len() != 4 {
171 return Err(InvalidNoteInputsError::WrongNumInputs(note.id(), 4).into());
172 }
173
174 let recall_height_felt = note_inputs[2];
175
176 let sender = note.metadata().sender();
177 let recall_height: u32 = recall_height_felt.as_int().try_into().map_err(|_err| {
178 InvalidNoteInputsError::BlockNumberError(note.id(), recall_height_felt.as_int())
179 })?;
180
181 if sender == *account_id {
182 Ok(Some(NoteRelevance::After(recall_height)))
183 } else {
184 Ok(None)
185 }
186 }
187}
188
189#[async_trait(?Send)]
193impl<AUTH> OnNoteReceived for NoteScreener<AUTH>
194where
195 AUTH: TransactionAuthenticator + Sync,
196{
197 async fn on_note_received(
202 &self,
203 committed_note: CommittedNote,
204 public_note: Option<InputNoteRecord>,
205 ) -> Result<NoteUpdateAction, ClientError> {
206 let note_id = *committed_note.note_id();
207
208 let input_note_present =
209 !self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.is_empty();
210 let output_note_present =
211 !self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.is_empty();
212
213 if input_note_present || output_note_present {
214 return Ok(NoteUpdateAction::Commit(committed_note));
216 }
217
218 match public_note {
219 Some(public_note) => {
220 if let Some(metadata) = public_note.metadata()
222 && self.store.get_unique_note_tags().await?.contains(&metadata.tag())
223 {
224 return Ok(NoteUpdateAction::Insert(public_note));
225 }
226
227 let new_note_relevance = self
229 .check_relevance(
230 &public_note
231 .clone()
232 .try_into()
233 .map_err(ClientError::NoteRecordConversionError)?,
234 )
235 .await?;
236 let is_relevant = !new_note_relevance.is_empty();
237 if is_relevant {
238 Ok(NoteUpdateAction::Insert(public_note))
239 } else {
240 Ok(NoteUpdateAction::Discard)
241 }
242 },
243 None => {
244 Ok(NoteUpdateAction::Discard)
247 },
248 }
249 }
250}
251
252#[derive(Debug, Error)]
257pub enum NoteScreenerError {
258 #[error("error while processing note inputs")]
259 InvalidNoteInputsError(#[from] InvalidNoteInputsError),
260 #[error("account data wasn't found for account id {0}")]
261 AccountDataNotFound(AccountId),
262 #[error("error while fetching data from the store")]
263 StoreError(#[from] StoreError),
264 #[error("error while checking note")]
265 NoteCheckerError(#[from] NoteCheckerError),
266 #[error("error while building transaction request")]
267 TransactionRequestError(#[from] TransactionRequestError),
268}
269
270#[derive(Debug, Error)]
271pub enum InvalidNoteInputsError {
272 #[error("account error for note with id {0}: {1}")]
273 AccountError(NoteId, AccountError),
274 #[error("asset error for note with id {0}: {1}")]
275 AssetError(NoteId, AssetError),
276 #[error("expected {1} note inputs for note with id {0}")]
277 WrongNumInputs(NoteId, usize),
278 #[error("note input representing block with value {1} for note with id {0}")]
279 BlockNumberError(NoteId, u64),
280}