miden_client/note/
note_screener.rs1use alloc::boxed::Box;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4
5use async_trait::async_trait;
6use miden_protocol::account::{Account, AccountId};
7use miden_protocol::errors::{AccountError, AssetError};
8use miden_protocol::note::{Note, NoteId};
9use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt};
10use miden_standards::note::NoteConsumptionStatus;
11use miden_tx::auth::TransactionAuthenticator;
12use miden_tx::{NoteCheckerError, NoteConsumptionChecker, TransactionExecutor};
13use thiserror::Error;
14
15use crate::ClientError;
16use crate::rpc::domain::note::CommittedNote;
17use crate::store::data_store::ClientDataStore;
18use crate::store::{InputNoteRecord, NoteFilter, Store, StoreError};
19use crate::sync::{NoteUpdateAction, OnNoteReceived};
20use crate::transaction::{InputNote, TransactionRequestBuilder, TransactionRequestError};
21
22pub type NoteConsumability = (AccountId, NoteConsumptionStatus);
27
28#[derive(Clone)]
35pub struct NoteScreener<AUTH> {
36 store: Arc<dyn Store>,
38 authenticator: Option<Arc<AUTH>>,
40}
41
42impl<AUTH> NoteScreener<AUTH>
43where
44 AUTH: TransactionAuthenticator + Sync,
45{
46 pub fn new(store: Arc<dyn Store>, authenticator: Option<Arc<AUTH>>) -> Self {
47 Self { store, authenticator }
48 }
49
50 pub async fn check_relevance(
56 &self,
57 note: &Note,
58 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
59 let mut note_relevances = vec![];
60 for id in self.store.get_account_ids().await? {
61 let account_record = self
62 .store
63 .get_account(id)
64 .await?
65 .ok_or(NoteScreenerError::AccountDataNotFound(id))?;
66 let account: Account = account_record
67 .try_into()
68 .map_err(|_| NoteScreenerError::AccountDataNotFound(id))?;
69
70 match self.check_standard_consumability(&account, note).await? {
71 NoteConsumptionStatus::NeverConsumable(_)
72 | NoteConsumptionStatus::UnconsumableConditions => {},
73 relevance => {
74 note_relevances.push((id, relevance));
75 },
76 }
77 }
78
79 Ok(note_relevances)
80 }
81
82 pub async fn check_standard_consumability(
85 &self,
86 account: &Account,
87 note: &Note,
88 ) -> Result<NoteConsumptionStatus, NoteScreenerError> {
89 let transaction_request =
90 TransactionRequestBuilder::new().build_consume_notes(vec![note.clone()])?;
91
92 let tx_script = transaction_request
93 .build_transaction_script(&AccountInterface::from_account(account))?;
94
95 let tx_args = transaction_request.clone().into_transaction_args(tx_script);
96
97 let data_store = ClientDataStore::new(self.store.clone());
98 let mut transaction_executor = TransactionExecutor::new(&data_store);
99 if let Some(authenticator) = &self.authenticator {
100 transaction_executor = transaction_executor.with_authenticator(authenticator.as_ref());
101 }
102
103 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
104
105 data_store.mast_store().load_account_code(account.code());
106 let note_consumption_check = consumption_checker
107 .can_consume(
108 account.id(),
109 self.store.get_sync_height().await?,
110 InputNote::unauthenticated(note.clone()),
111 tx_args,
112 )
113 .await?;
114
115 Ok(note_consumption_check)
116 }
117}
118
119#[async_trait(?Send)]
123impl<AUTH> OnNoteReceived for NoteScreener<AUTH>
124where
125 AUTH: TransactionAuthenticator + Sync,
126{
127 async fn on_note_received(
132 &self,
133 committed_note: CommittedNote,
134 public_note: Option<InputNoteRecord>,
135 ) -> Result<NoteUpdateAction, ClientError> {
136 let note_id = *committed_note.note_id();
137
138 let input_note_present =
139 !self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.is_empty();
140 let output_note_present =
141 !self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.is_empty();
142
143 if input_note_present || output_note_present {
144 return Ok(NoteUpdateAction::Commit(committed_note));
146 }
147
148 match public_note {
149 Some(public_note) => {
150 if let Some(metadata) = public_note.metadata()
152 && self.store.get_unique_note_tags().await?.contains(&metadata.tag())
153 {
154 return Ok(NoteUpdateAction::Insert(public_note));
155 }
156
157 let new_note_relevance = self
159 .check_relevance(
160 &public_note
161 .clone()
162 .try_into()
163 .map_err(ClientError::NoteRecordConversionError)?,
164 )
165 .await?;
166 let is_relevant = !new_note_relevance.is_empty();
167 if is_relevant {
168 Ok(NoteUpdateAction::Insert(public_note))
169 } else {
170 Ok(NoteUpdateAction::Discard)
171 }
172 },
173 None => {
174 Ok(NoteUpdateAction::Discard)
177 },
178 }
179 }
180}
181
182#[derive(Debug, Error)]
187pub enum NoteScreenerError {
188 #[error("error while processing note inputs")]
189 InvalidNoteInputsError(#[from] InvalidNoteInputsError),
190 #[error("account data wasn't found for account id {0}")]
191 AccountDataNotFound(AccountId),
192 #[error("error while fetching data from the store")]
193 StoreError(#[from] StoreError),
194 #[error("error while checking note")]
195 NoteCheckerError(#[from] NoteCheckerError),
196 #[error("error while building transaction request")]
197 TransactionRequestError(#[from] TransactionRequestError),
198}
199
200#[derive(Debug, Error)]
201pub enum InvalidNoteInputsError {
202 #[error("account error for note with id {0}: {1}")]
203 AccountError(NoteId, AccountError),
204 #[error("asset error for note with id {0}: {1}")]
205 AssetError(NoteId, AssetError),
206 #[error("expected {1} note inputs for note with id {0}")]
207 WrongNumInputs(NoteId, usize),
208 #[error("note input representing block with value {1} for note with id {0}")]
209 BlockNumberError(NoteId, u64),
210}