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::{NoteCheckerError, NoteConsumptionChecker, TransactionExecutor};
12use thiserror::Error;
13
14use crate::ClientError;
15use crate::rpc::domain::note::CommittedNote;
16use crate::store::data_store::ClientDataStore;
17use crate::store::{InputNoteRecord, NoteFilter, Store, StoreError};
18use crate::sync::{NoteUpdateAction, OnNoteReceived};
19use crate::transaction::{InputNote, TransactionRequestBuilder, TransactionRequestError};
20
21pub type NoteConsumability = (AccountId, NoteConsumptionStatus);
26
27#[derive(Clone)]
35pub struct NoteScreener {
36 store: Arc<dyn Store>,
38}
39
40impl NoteScreener {
41 pub fn new(store: Arc<dyn Store>) -> Self {
42 Self { store }
43 }
44
45 pub async fn check_relevance(
51 &self,
52 note: &Note,
53 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
54 let mut note_relevances = vec![];
55 for id in self.store.get_account_ids().await? {
56 let account_record = self
57 .store
58 .get_account(id)
59 .await?
60 .ok_or(NoteScreenerError::AccountDataNotFound(id))?;
61 let account: Account = account_record
62 .try_into()
63 .map_err(|_| NoteScreenerError::AccountDataNotFound(id))?;
64
65 match self.check_standard_consumability(&account, note).await? {
66 NoteConsumptionStatus::NeverConsumable(_)
67 | NoteConsumptionStatus::UnconsumableConditions => {},
68 relevance => {
69 note_relevances.push((id, relevance));
70 },
71 }
72 }
73
74 Ok(note_relevances)
75 }
76
77 pub async fn check_standard_consumability(
80 &self,
81 account: &Account,
82 note: &Note,
83 ) -> Result<NoteConsumptionStatus, NoteScreenerError> {
84 let transaction_request =
85 TransactionRequestBuilder::new().build_consume_notes(vec![note.clone()])?;
86
87 let tx_script = transaction_request
88 .build_transaction_script(&AccountInterface::from_account(account))?;
89
90 let tx_args = transaction_request.clone().into_transaction_args(tx_script);
91
92 let data_store = ClientDataStore::new(self.store.clone());
93 let transaction_executor: TransactionExecutor<'_, '_, _, ()> =
100 TransactionExecutor::new(&data_store);
101
102 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
103
104 data_store.mast_store().load_account_code(account.code());
105 let note_consumption_check = consumption_checker
106 .can_consume(
107 account.id(),
108 self.store.get_sync_height().await?,
109 InputNote::unauthenticated(note.clone()),
110 tx_args,
111 )
112 .await?;
113
114 Ok(note_consumption_check)
115 }
116}
117
118#[async_trait(?Send)]
122impl OnNoteReceived for NoteScreener {
123 async fn on_note_received(
128 &self,
129 committed_note: CommittedNote,
130 public_note: Option<InputNoteRecord>,
131 ) -> Result<NoteUpdateAction, ClientError> {
132 let note_id = *committed_note.note_id();
133
134 let input_note_present =
135 !self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.is_empty();
136 let output_note_present =
137 !self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.is_empty();
138
139 if input_note_present || output_note_present {
140 return Ok(NoteUpdateAction::Commit(committed_note));
142 }
143
144 match public_note {
145 Some(public_note) => {
146 if let Some(metadata) = public_note.metadata()
148 && self.store.get_unique_note_tags().await?.contains(&metadata.tag())
149 {
150 return Ok(NoteUpdateAction::Insert(public_note));
151 }
152
153 let new_note_relevance = self
155 .check_relevance(
156 &public_note
157 .clone()
158 .try_into()
159 .map_err(ClientError::NoteRecordConversionError)?,
160 )
161 .await?;
162 let is_relevant = !new_note_relevance.is_empty();
163 if is_relevant {
164 Ok(NoteUpdateAction::Insert(public_note))
165 } else {
166 Ok(NoteUpdateAction::Discard)
167 }
168 },
169 None => {
170 Ok(NoteUpdateAction::Discard)
173 },
174 }
175 }
176}
177
178#[derive(Debug, Error)]
183pub enum NoteScreenerError {
184 #[error("failed to process note inputs")]
185 InvalidNoteInputsError(#[from] InvalidNoteInputsError),
186 #[error("account {0} data not found in the store")]
187 AccountDataNotFound(AccountId),
188 #[error("failed to fetch data from the store")]
189 StoreError(#[from] StoreError),
190 #[error("note consumption check failed")]
191 NoteCheckerError(#[from] NoteCheckerError),
192 #[error("failed to build transaction request")]
193 TransactionRequestError(#[from] TransactionRequestError),
194}
195
196#[derive(Debug, Error)]
197pub enum InvalidNoteInputsError {
198 #[error("account error for note with id {0}: {1}")]
199 AccountError(NoteId, AccountError),
200 #[error("asset error for note with id {0}: {1}")]
201 AssetError(NoteId, AssetError),
202 #[error("expected {1} note inputs for note with id {0}")]
203 WrongNumInputs(NoteId, usize),
204 #[error("note input representing block with value {1} for note with id {0}")]
205 BlockNumberError(NoteId, u64),
206}