Skip to main content

miden_client/note/
note_screener.rs

1use alloc::boxed::Box;
2use alloc::collections::BTreeMap;
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use async_trait::async_trait;
7use miden_protocol::account::{AccountCode, AccountId};
8use miden_protocol::note::{Note, NoteId};
9use miden_standards::note::NoteConsumptionStatus;
10use miden_tx::auth::TransactionAuthenticator;
11use miden_tx::{
12    NoteCheckerError,
13    NoteConsumptionChecker,
14    NoteConsumptionInfo,
15    TransactionExecutor,
16};
17use thiserror::Error;
18
19use crate::ClientError;
20use crate::rpc::domain::note::CommittedNote;
21use crate::store::data_store::ClientDataStore;
22use crate::store::{InputNoteRecord, NoteFilter, Store, StoreError};
23use crate::sync::{NoteUpdateAction, OnNoteReceived};
24use crate::transaction::{AdviceMap, InputNote, TransactionArgs, TransactionRequestError};
25
26/// Represents the consumability of a note by a specific account.
27///
28/// The tuple contains the account ID that may consume the note and the moment it will become
29/// relevant.
30pub type NoteConsumability = (AccountId, NoteConsumptionStatus);
31
32/// Returns `true` if the consumption status indicates that the note may be consumable by the
33/// account. A note is considered relevant unless it is permanently unconsumable (either due to
34/// a fundamental incompatibility or unconsumable conditions).
35fn is_relevant(consumption_status: &NoteConsumptionStatus) -> bool {
36    !matches!(
37        consumption_status,
38        NoteConsumptionStatus::NeverConsumable(_) | NoteConsumptionStatus::UnconsumableConditions
39    )
40}
41
42/// Provides functionality for testing whether a note is relevant to the client or not.
43///
44/// Here, relevance is based on whether the note is able to be consumed by an account that is
45/// tracked in the provided `store`. This can be derived in a number of ways, such as looking
46/// at the combination of script root and note inputs. For example, a P2ID note is relevant
47/// for a specific account ID if this ID is its first note input.
48#[derive(Clone)]
49pub struct NoteScreener<AUTH> {
50    /// A reference to the client's store, used to fetch necessary data to check consumability.
51    store: Arc<dyn Store>,
52    /// A reference to the transaction authenticator
53    authenticator: Option<Arc<AUTH>>,
54    /// Optional transaction arguments to use when checking consumability.
55    tx_args: Option<TransactionArgs>,
56}
57
58impl<AUTH> NoteScreener<AUTH>
59where
60    AUTH: TransactionAuthenticator + Sync,
61{
62    pub fn new(store: Arc<dyn Store>, authenticator: Option<Arc<AUTH>>) -> Self {
63        Self { store, authenticator, tx_args: None }
64    }
65
66    /// Sets the transaction arguments to use when checking note consumability.
67    /// If not set, a default `TransactionArgs` with an empty advice map is used.
68    #[must_use]
69    pub fn with_transaction_args(mut self, tx_args: TransactionArgs) -> Self {
70        self.tx_args = Some(tx_args);
71        self
72    }
73
74    fn tx_args(&self) -> TransactionArgs {
75        self.tx_args
76            .clone()
77            .unwrap_or_else(|| TransactionArgs::new(AdviceMap::default()))
78    }
79
80    /// Checks whether the provided note could be consumed by any of the accounts tracked by
81    /// this screener. Convenience wrapper around [`Self::can_consume_batch`] for a single note.
82    ///
83    /// Returns the [`NoteConsumptionStatus`] for each account that could consume the note.
84    pub async fn can_consume(
85        &self,
86        note: &Note,
87    ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
88        Ok(self
89            .can_consume_batch(core::slice::from_ref(note))
90            .await?
91            .remove(&note.id())
92            .unwrap_or_default())
93    }
94
95    /// Checks whether the provided notes could be consumed by any of the accounts tracked by
96    /// this screener, by executing a transaction for each note-account pair.
97    ///
98    /// Returns a map from [`NoteId`] to a list of `(AccountId, NoteConsumptionStatus)` pairs.
99    /// Notes that are permanently unconsumable by all accounts are not included in the result.
100    pub async fn can_consume_batch(
101        &self,
102        notes: &[Note],
103    ) -> Result<BTreeMap<NoteId, Vec<NoteConsumability>>, NoteScreenerError> {
104        let account_ids = self.store.get_account_ids().await?;
105        if notes.is_empty() || account_ids.is_empty() {
106            return Ok(BTreeMap::new());
107        }
108
109        let block_ref = self.store.get_sync_height().await?;
110        let mut relevant_notes: BTreeMap<NoteId, Vec<NoteConsumability>> = BTreeMap::new();
111        let tx_args = self.tx_args();
112
113        let data_store = ClientDataStore::new(self.store.clone());
114        let mut transaction_executor = TransactionExecutor::new(&data_store);
115        if let Some(authenticator) = &self.authenticator {
116            transaction_executor = transaction_executor.with_authenticator(authenticator.as_ref());
117        }
118        let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
119
120        for account_id in account_ids {
121            let account_code = self.get_account_code(account_id).await?;
122            data_store.mast_store().load_account_code(&account_code);
123
124            for note in notes {
125                let consumption_status = consumption_checker
126                    .can_consume(
127                        account_id,
128                        block_ref,
129                        InputNote::unauthenticated(note.clone()),
130                        tx_args.clone(),
131                    )
132                    .await?;
133
134                if is_relevant(&consumption_status) {
135                    relevant_notes
136                        .entry(note.id())
137                        .or_default()
138                        .push((account_id, consumption_status));
139                }
140            }
141        }
142
143        Ok(relevant_notes)
144    }
145
146    /// Checks whether the provided notes could be consumed by a specific account by attempting
147    /// to execute them together in a transaction. Notes that fail are progressively removed
148    /// until a maximal set of successfully consumable notes is found.
149    ///
150    /// Returns a [`NoteConsumptionInfo`] splitting notes into those that succeeded and those
151    /// that failed.
152    pub async fn check_notes_consumability(
153        &self,
154        account_id: AccountId,
155        notes: Vec<Note>,
156    ) -> Result<NoteConsumptionInfo, NoteScreenerError> {
157        let block_ref = self.store.get_sync_height().await?;
158        let tx_args = self.tx_args();
159        let account_code = self.get_account_code(account_id).await?;
160
161        let data_store = ClientDataStore::new(self.store.clone());
162        let mut transaction_executor = TransactionExecutor::new(&data_store);
163        if let Some(authenticator) = &self.authenticator {
164            transaction_executor = transaction_executor.with_authenticator(authenticator.as_ref());
165        }
166
167        let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
168
169        data_store.mast_store().load_account_code(&account_code);
170        let note_consumption_info = consumption_checker
171            .check_notes_consumability(account_id, block_ref, notes, tx_args)
172            .await?;
173
174        Ok(note_consumption_info)
175    }
176
177    async fn get_account_code(
178        &self,
179        account_id: AccountId,
180    ) -> Result<AccountCode, NoteScreenerError> {
181        self.store
182            .get_account_code(account_id)
183            .await?
184            .ok_or(NoteScreenerError::AccountDataNotFound(account_id))
185    }
186}
187
188// DEFAULT CALLBACK IMPLEMENTATIONS
189// ================================================================================================
190
191#[async_trait(?Send)]
192impl<AUTH> OnNoteReceived for NoteScreener<AUTH>
193where
194    AUTH: TransactionAuthenticator + Sync,
195{
196    /// Default implementation of the [`OnNoteReceived`] callback. It queries the store for the
197    /// committed note to check if it's relevant. If the note wasn't being tracked but it came in
198    /// the sync response it may be a new public note, in that case we use the [`NoteScreener`]
199    /// to check its relevance.
200    async fn on_note_received(
201        &self,
202        committed_note: CommittedNote,
203        public_note: Option<InputNoteRecord>,
204    ) -> Result<NoteUpdateAction, ClientError> {
205        let note_id = *committed_note.note_id();
206
207        let input_note_present =
208            !self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.is_empty();
209        let output_note_present =
210            !self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.is_empty();
211
212        if input_note_present || output_note_present {
213            // The note is being tracked by the client so it is relevant
214            return Ok(NoteUpdateAction::Commit(committed_note));
215        }
216
217        match public_note {
218            Some(public_note) => {
219                // If tracked by the user, keep note regardless of inputs and extra checks
220                if let Some(metadata) = public_note.metadata()
221                    && self.store.get_unique_note_tags().await?.contains(&metadata.tag())
222                {
223                    return Ok(NoteUpdateAction::Insert(public_note));
224                }
225
226                // The note is not being tracked by the client and is public so we can screen it
227                let new_note_relevance = self
228                    .can_consume(
229                        &public_note
230                            .clone()
231                            .try_into()
232                            .map_err(ClientError::NoteRecordConversionError)?,
233                    )
234                    .await?;
235                let is_relevant = !new_note_relevance.is_empty();
236                if is_relevant {
237                    Ok(NoteUpdateAction::Insert(public_note))
238                } else {
239                    Ok(NoteUpdateAction::Discard)
240                }
241            },
242            None => {
243                // The note is not being tracked by the client and is private so we can't determine
244                // if it is relevant
245                Ok(NoteUpdateAction::Discard)
246            },
247        }
248    }
249}
250
251// NOTE SCREENER ERRORS
252// ================================================================================================
253
254/// Error when screening notes to check relevance to a client.
255#[derive(Debug, Error)]
256pub enum NoteScreenerError {
257    #[error("account {0} data not found in the store")]
258    AccountDataNotFound(AccountId),
259    #[error("failed to fetch data from the store")]
260    StoreError(#[from] StoreError),
261    #[error("note consumption check failed")]
262    NoteCheckerError(#[from] NoteCheckerError),
263    #[error("failed to build transaction request")]
264    TransactionRequestError(#[from] TransactionRequestError),
265}