miden_client/note/
note_screener.rs1use alloc::{sync::Arc, vec::Vec};
2use core::fmt;
3
4use miden_lib::{account::interface::AccountInterface, note::well_known_note::WellKnownNote};
5use miden_objects::{
6 AccountError, AssetError,
7 account::{Account, AccountId},
8 assembly::DefaultSourceManager,
9 note::{Note, NoteId},
10 transaction::{InputNote, InputNotes},
11};
12use miden_tx::{
13 NoteAccountExecution, NoteConsumptionChecker, TransactionExecutor, TransactionExecutorError,
14 auth::TransactionAuthenticator,
15};
16use thiserror::Error;
17
18use crate::{
19 store::{Store, StoreError, data_store::ClientDataStore},
20 transaction::{TransactionRequestBuilder, TransactionRequestError},
21};
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
47pub struct NoteScreener {
54 store: Arc<dyn Store>,
56 authenticator: Option<Arc<dyn TransactionAuthenticator>>,
58}
59
60impl NoteScreener {
61 pub fn new(
62 store: Arc<dyn Store>,
63 authenticator: Option<Arc<dyn TransactionAuthenticator>>,
64 ) -> Self {
65 Self { store, authenticator }
66 }
67
68 pub async fn check_relevance(
77 &self,
78 note: &Note,
79 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
80 let mut note_relevances = vec![];
81 for id in self.store.get_account_ids().await? {
82 let account_record = self
83 .store
84 .get_account(id)
85 .await?
86 .ok_or(NoteScreenerError::AccountDataNotFound(id))?;
87
88 match self.check_standard_consumability(account_record.account(), note).await {
89 Ok(Some(relevance)) => {
90 note_relevances.push((id, relevance));
91 },
92 Ok(None) => {
93 let script_root = note.script().root();
96
97 if script_root == WellKnownNote::P2IDE.script_root() {
98 if let Some(relevance) = Self::check_p2ide_recall_consumability(note, &id)?
99 {
100 note_relevances.push((id, relevance));
101 }
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 =
124 transaction_request.build_transaction_script(&AccountInterface::from(account), true)?;
125
126 let tx_args = transaction_request.clone().into_transaction_args(tx_script, vec![]);
127 let input_notes = InputNotes::new(vec![InputNote::unauthenticated(note.clone())])
128 .expect("Single note should be valid");
129
130 let data_store = ClientDataStore::new(self.store.clone());
131 let transaction_executor =
132 TransactionExecutor::new(&data_store, self.authenticator.as_deref());
133 let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
134
135 data_store.mast_store().load_account_code(account.code());
136
137 if let NoteAccountExecution::Success = consumption_checker
138 .check_notes_consumability(
139 account.id(),
140 self.store.get_sync_height().await?,
141 input_notes,
142 tx_args,
143 Arc::new(DefaultSourceManager::default()),
144 )
145 .await?
146 {
147 return Ok(Some(NoteRelevance::Now));
148 }
149
150 Ok(None)
151 }
152
153 fn check_p2ide_recall_consumability(
156 note: &Note,
157 account_id: &AccountId,
158 ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
159 let note_inputs = note.inputs().values();
160 if note_inputs.len() != 4 {
161 return Err(InvalidNoteInputsError::WrongNumInputs(note.id(), 4).into());
162 }
163
164 let recall_height_felt = note_inputs[2];
165
166 let sender = note.metadata().sender();
167 let recall_height: u32 = recall_height_felt.as_int().try_into().map_err(|_err| {
168 InvalidNoteInputsError::BlockNumberError(note.id(), recall_height_felt.as_int())
169 })?;
170
171 if sender == *account_id {
172 Ok(Some(NoteRelevance::After(recall_height)))
173 } else {
174 Ok(None)
175 }
176 }
177}
178
179#[derive(Debug, Error)]
184pub enum NoteScreenerError {
185 #[error("error while processing note inputs")]
186 InvalidNoteInputsError(#[from] InvalidNoteInputsError),
187 #[error("account data wasn't found for account id {0}")]
188 AccountDataNotFound(AccountId),
189 #[error("error while fetching data from the store")]
190 StoreError(#[from] StoreError),
191 #[error("error while checking consume transaction")]
192 TransactionExecutionError(#[from] TransactionExecutorError),
193 #[error("error while building consume transaction request")]
194 TransactionRequestError(#[from] TransactionRequestError),
195}
196
197#[derive(Debug, Error)]
198pub enum InvalidNoteInputsError {
199 #[error("account error for note with id {0}: {1}")]
200 AccountError(NoteId, AccountError),
201 #[error("asset error for note with id {0}: {1}")]
202 AssetError(NoteId, AssetError),
203 #[error("expected {1} note inputs for note with id {0}")]
204 WrongNumInputs(NoteId, usize),
205 #[error("note input representing block with value {1} for note with id {0}")]
206 BlockNumberError(NoteId, u64),
207}