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::{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
21/// Represents the consumability of a note by a specific account.
22///
23/// The tuple contains the account ID that may consume the note and the moment it will become
24/// relevant.
25pub type NoteConsumability = (AccountId, NoteConsumptionStatus);
26
27/// Provides functionality for testing whether a note is relevant to the client or not.
28///
29/// Relevance is based on whether the note is able to be consumed by an account tracked in
30/// the provided `store`. The screener trial-executes a consume transaction **without** an
31/// authenticator so that external signers (e.g. wallet extensions) are never invoked during
32/// screening. For accounts that require authentication the checker returns
33/// [`NoteConsumptionStatus::ConsumableWithAuthorization`].
34#[derive(Clone)]
35pub struct NoteScreener {
36    /// A reference to the client's store, used to fetch necessary data to check consumability.
37    store: Arc<dyn Store>,
38}
39
40impl NoteScreener {
41    pub fn new(store: Arc<dyn Store>) -> Self {
42        Self { store }
43    }
44
45    /// Returns a vector of tuples describing the relevance of the provided note to the
46    /// accounts monitored by this screener.
47    ///
48    /// The relevance is determined by [`NoteConsumptionChecker::can_consume`] and is based on
49    /// current conditions (for example, it takes the latest block in the client as reference).
50    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    /// Tries to execute a standard consume transaction to check if the note is consumable by the
78    /// account.
79    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        // Don't attach the real authenticator for consumability checks. The
94        // NoteConsumptionChecker gracefully handles a missing authenticator by
95        // returning `ConsumableWithAuthorization` instead of calling
96        // `get_signature()`. Attaching the real authenticator here causes the
97        // external signer (e.g. wallet extension) to be invoked during
98        // sync_state, producing unwanted confirmation popups on every sync.
99        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// DEFAULT CALLBACK IMPLEMENTATIONS
119// ================================================================================================
120
121#[async_trait(?Send)]
122impl OnNoteReceived for NoteScreener {
123    /// Default implementation of the [`OnNoteReceived`] callback. It queries the store for the
124    /// committed note to check if it's relevant. If the note wasn't being tracked but it came in
125    /// the sync response it may be a new public note, in that case we use the [`NoteScreener`]
126    /// to check its relevance.
127    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            // The note is being tracked by the client so it is relevant
141            return Ok(NoteUpdateAction::Commit(committed_note));
142        }
143
144        match public_note {
145            Some(public_note) => {
146                // If tracked by the user, keep note regardless of inputs and extra checks
147                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                // The note is not being tracked by the client and is public so we can screen it
154                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                // The note is not being tracked by the client and is private so we can't determine
171                // if it is relevant
172                Ok(NoteUpdateAction::Discard)
173            },
174        }
175    }
176}
177
178// NOTE SCREENER ERRORS
179// ================================================================================================
180
181/// Error when screening notes to check relevance to a client.
182#[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}