miden_client/note/
note_screener.rs1use alloc::boxed::Box;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4use core::fmt;
5
6use async_trait::async_trait;
7use miden_lib::account::interface::AccountInterface;
8use miden_lib::note::{NoteConsumptionStatus, WellKnownNote};
9use miden_objects::account::{Account, AccountId};
10use miden_objects::note::{Note, NoteId};
11use miden_objects::{AccountError, AssetError};
12use miden_tx::auth::TransactionAuthenticator;
13use miden_tx::{NoteCheckerError, NoteConsumptionChecker, TransactionExecutor};
14use thiserror::Error;
15
16use crate::ClientError;
17use crate::rpc::domain::note::CommittedNote;
18use crate::store::data_store::ClientDataStore;
19use crate::store::{InputNoteRecord, NoteFilter, Store, StoreError};
20use crate::sync::{NoteUpdateAction, OnNoteReceived};
21use crate::transaction::{InputNote, TransactionRequestBuilder, TransactionRequestError};
22
23#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
25pub enum NoteRelevance {
26 Now,
28 After(u32),
30}
31
32pub type NoteConsumability = (AccountId, NoteRelevance);
37
38impl fmt::Display for NoteRelevance {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 NoteRelevance::Now => write!(f, "Now"),
42 NoteRelevance::After(height) => write!(f, "After block {height}"),
43 }
44 }
45}
46
47#[derive(Clone)]
54pub 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(
74 &self,
75 note: &Note,
76 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
77 let mut note_relevances = vec![];
78 for id in self.store.get_account_ids().await? {
79 let account_record = self
80 .store
81 .get_account(id)
82 .await?
83 .ok_or(NoteScreenerError::AccountDataNotFound(id))?;
84
85 match self.check_standard_consumability(account_record.account(), note).await {
86 Ok(Some(relevance)) => {
87 note_relevances.push((id, relevance));
88 },
89 Ok(None) => {
90 let script_root = note.script().root();
93
94 if script_root == WellKnownNote::P2IDE.script_root()
95 && let Some(relevance) = Self::check_p2ide_recall_consumability(note, &id)?
96 {
97 note_relevances.push((id, relevance));
98 }
99 },
100 Err(_) => {},
103 }
104 }
105
106 Ok(note_relevances)
107 }
108
109 async fn check_standard_consumability(
112 &self,
113 account: &Account,
114 note: &Note,
115 ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
116 let transaction_request =
117 TransactionRequestBuilder::new().build_consume_notes(vec![note.id()])?;
118
119 let tx_script = transaction_request.build_transaction_script(
120 &AccountInterface::from(account),
121 crate::DebugMode::Disabled,
122 )?;
123
124 let tx_args = transaction_request.clone().into_transaction_args(tx_script);
125
126 let data_store = ClientDataStore::new(self.store.clone());
127 let mut transaction_executor = TransactionExecutor::new(&data_store);
128 if let Some(authenticator) = &self.authenticator {
129 transaction_executor = transaction_executor.with_authenticator(authenticator.as_ref());
130 }
131
132 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
133
134 data_store.mast_store().load_account_code(account.code());
135 let note_consumption_check = consumption_checker
136 .can_consume(
137 account.id(),
138 self.store.get_sync_height().await?,
139 InputNote::unauthenticated(note.clone()),
140 tx_args,
141 )
142 .await?;
143
144 let result = match note_consumption_check {
145 NoteConsumptionStatus::ConsumableAfter(block_number) => {
146 Some(NoteRelevance::After(block_number.as_u32()))
147 },
148 NoteConsumptionStatus::Consumable
149 | NoteConsumptionStatus::ConsumableWithAuthorization => Some(NoteRelevance::Now),
150 NoteConsumptionStatus::UnconsumableConditions
154 | NoteConsumptionStatus::NeverConsumable(_) => None,
155 };
156 Ok(result)
157 }
158
159 fn check_p2ide_recall_consumability(
162 note: &Note,
163 account_id: &AccountId,
164 ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
165 let note_inputs = note.inputs().values();
166 if note_inputs.len() != 4 {
169 return Err(InvalidNoteInputsError::WrongNumInputs(note.id(), 4).into());
170 }
171
172 let recall_height_felt = note_inputs[2];
173
174 let sender = note.metadata().sender();
175 let recall_height: u32 = recall_height_felt.as_int().try_into().map_err(|_err| {
176 InvalidNoteInputsError::BlockNumberError(note.id(), recall_height_felt.as_int())
177 })?;
178
179 if sender == *account_id {
180 Ok(Some(NoteRelevance::After(recall_height)))
181 } else {
182 Ok(None)
183 }
184 }
185}
186
187#[async_trait(?Send)]
191impl<AUTH> OnNoteReceived for NoteScreener<AUTH>
192where
193 AUTH: TransactionAuthenticator + Sync,
194{
195 async fn on_note_received(
200 &self,
201 committed_note: CommittedNote,
202 public_note: Option<InputNoteRecord>,
203 ) -> Result<NoteUpdateAction, ClientError> {
204 let note_id = *committed_note.note_id();
205
206 let input_note_present =
207 !self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.is_empty();
208 let output_note_present =
209 !self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.is_empty();
210
211 if input_note_present || output_note_present {
212 return Ok(NoteUpdateAction::Commit(committed_note));
214 }
215
216 match public_note {
217 Some(public_note) => {
218 if let Some(metadata) = public_note.metadata()
220 && self.store.get_unique_note_tags().await?.contains(&metadata.tag())
221 {
222 return Ok(NoteUpdateAction::Insert(public_note));
223 }
224
225 let new_note_relevance = self
227 .check_relevance(
228 &public_note
229 .clone()
230 .try_into()
231 .map_err(ClientError::NoteRecordConversionError)?,
232 )
233 .await?;
234 let is_relevant = !new_note_relevance.is_empty();
235 if is_relevant {
236 Ok(NoteUpdateAction::Insert(public_note))
237 } else {
238 Ok(NoteUpdateAction::Discard)
239 }
240 },
241 None => {
242 Ok(NoteUpdateAction::Discard)
245 },
246 }
247 }
248}
249
250#[derive(Debug, Error)]
255pub enum NoteScreenerError {
256 #[error("error while processing note inputs")]
257 InvalidNoteInputsError(#[from] InvalidNoteInputsError),
258 #[error("account data wasn't found for account id {0}")]
259 AccountDataNotFound(AccountId),
260 #[error("error while fetching data from the store")]
261 StoreError(#[from] StoreError),
262 #[error("error while checking note")]
263 NoteCheckerError(#[from] NoteCheckerError),
264 #[error("error while building transaction request")]
265 TransactionRequestError(#[from] TransactionRequestError),
266}
267
268#[derive(Debug, Error)]
269pub enum InvalidNoteInputsError {
270 #[error("account error for note with id {0}: {1}")]
271 AccountError(NoteId, AccountError),
272 #[error("asset error for note with id {0}: {1}")]
273 AssetError(NoteId, AssetError),
274 #[error("expected {1} note inputs for note with id {0}")]
275 WrongNumInputs(NoteId, usize),
276 #[error("note input representing block with value {1} for note with id {0}")]
277 BlockNumberError(NoteId, u64),
278}