Skip to main content

miden_client/note/
note_screener.rs

1use 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
22/// Represents the consumability of a note by a specific account.
23///
24/// The tuple contains the account ID that may consume the note and the moment it will become
25/// relevant.
26pub type NoteConsumability = (AccountId, NoteConsumptionStatus);
27
28/// Provides functionality for testing whether a note is relevant to the client or not.
29///
30/// Here, relevance is based on whether the note is able to be consumed by an account that is
31/// tracked in the provided `store`. This can be derived in a number of ways, such as looking
32/// at the combination of script root and note inputs. For example, a P2ID note is relevant
33/// for a specific account ID if this ID is its first note input.
34#[derive(Clone)]
35pub struct NoteScreener<AUTH> {
36    /// A reference to the client's store, used to fetch necessary data to check consumability.
37    store: Arc<dyn Store>,
38    /// A reference to the transaction authenticator
39    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    /// Returns a vector of tuples describing the relevance of the provided note to the
51    /// accounts monitored by this screener.
52    ///
53    /// The relevance is determined by [`NoteConsumptionChecker::can_consume`] and is based on
54    /// current conditions (for example, it takes the latest block in the client as reference).
55    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    /// Tries to execute a standard consume transaction to check if the note is consumable by the
83    /// account.
84    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// DEFAULT CALLBACK IMPLEMENTATIONS
120// ================================================================================================
121
122#[async_trait(?Send)]
123impl<AUTH> OnNoteReceived for NoteScreener<AUTH>
124where
125    AUTH: TransactionAuthenticator + Sync,
126{
127    /// Default implementation of the [`OnNoteReceived`] callback. It queries the store for the
128    /// committed note to check if it's relevant. If the note wasn't being tracked but it came in
129    /// the sync response it may be a new public note, in that case we use the [`NoteScreener`]
130    /// to check its relevance.
131    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            // The note is being tracked by the client so it is relevant
145            return Ok(NoteUpdateAction::Commit(committed_note));
146        }
147
148        match public_note {
149            Some(public_note) => {
150                // If tracked by the user, keep note regardless of inputs and extra checks
151                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                // The note is not being tracked by the client and is public so we can screen it
158                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                // The note is not being tracked by the client and is private so we can't determine
175                // if it is relevant
176                Ok(NoteUpdateAction::Discard)
177            },
178        }
179    }
180}
181
182// NOTE SCREENER ERRORS
183// ================================================================================================
184
185/// Error when screening notes to check relevance to a client.
186#[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}