1use alloc::{collections::BTreeSet, string::ToString, vec::Vec};
2use core::fmt;
3
4use miden_objects::{
5 account::{Account, AccountId},
6 asset::Asset,
7 note::{Note, NoteId},
8 AccountError, AssetError, Felt, Word,
9};
10use thiserror::Error;
11
12use super::script_roots::{P2ID, P2IDR, SWAP};
13use crate::store::{Store, StoreError};
14
15#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
17pub enum NoteRelevance {
18 Always,
20 After(u32),
22}
23
24pub type NoteConsumability = (AccountId, NoteRelevance);
29
30impl fmt::Display for NoteRelevance {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 NoteRelevance::Always => write!(f, "Always"),
34 NoteRelevance::After(height) => write!(f, "After block {}", height),
35 }
36 }
37}
38
39pub struct NoteScreener {
46 store: alloc::sync::Arc<dyn Store>,
47}
48
49impl NoteScreener {
50 pub fn new(store: alloc::sync::Arc<dyn Store>) -> Self {
51 Self { store }
52 }
53
54 pub async fn check_relevance(
61 &self,
62 note: &Note,
63 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
64 let account_ids = BTreeSet::from_iter(self.store.get_account_ids().await?);
65
66 let script_hash = note.script().hash().to_string();
67 let note_relevance = match script_hash.as_str() {
68 P2ID => Self::check_p2id_relevance(note, &account_ids)?,
69 P2IDR => Self::check_p2idr_relevance(note, &account_ids)?,
70 SWAP => self.check_swap_relevance(note, &account_ids).await?,
71 _ => self.check_script_relevance(note, &account_ids)?,
72 };
73
74 Ok(note_relevance)
75 }
76
77 fn check_p2id_relevance(
78 note: &Note,
79 account_ids: &BTreeSet<AccountId>,
80 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
81 let note_inputs = note.inputs().values();
82 if note_inputs.len() != 2 {
83 return Err(InvalidNoteInputsError::WrongNumInputs(note.id(), 2).into());
84 }
85
86 let account_id_felts: [Felt; 2] = note_inputs[0..2].try_into().expect(
87 "Should be able to convert the first two note inputs to an array of two Felt elements",
88 );
89
90 let account_id =
91 AccountId::try_from([account_id_felts[1], account_id_felts[0]]).map_err(|err| {
92 InvalidNoteInputsError::AccountError(
93 note.id(),
94 AccountError::FinalAccountHeaderIdParsingFailed(err),
95 )
96 })?;
97
98 if !account_ids.contains(&account_id) {
99 return Ok(vec![]);
100 }
101 Ok(vec![(account_id, NoteRelevance::Always)])
102 }
103
104 fn check_p2idr_relevance(
105 note: &Note,
106 account_ids: &BTreeSet<AccountId>,
107 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
108 let note_inputs = note.inputs().values();
109 if note_inputs.len() != 3 {
110 return Err(InvalidNoteInputsError::WrongNumInputs(note.id(), 3).into());
111 }
112
113 let account_id_felts: [Felt; 2] = note_inputs[0..2].try_into().expect(
114 "Should be able to convert the first two note inputs to an array of two Felt elements",
115 );
116
117 let recall_height_felt = note_inputs[2];
118
119 let sender = note.metadata().sender();
120 let recall_height: u32 = recall_height_felt.as_int().try_into().map_err(|_err| {
121 InvalidNoteInputsError::BlockNumberError(note.id(), recall_height_felt.as_int())
122 })?;
123
124 let account_id =
125 AccountId::try_from([account_id_felts[1], account_id_felts[0]]).map_err(|err| {
126 InvalidNoteInputsError::AccountError(
127 note.id(),
128 AccountError::FinalAccountHeaderIdParsingFailed(err),
129 )
130 })?;
131
132 Ok(vec![
133 (account_id, NoteRelevance::Always),
134 (sender, NoteRelevance::After(recall_height)),
135 ]
136 .into_iter()
137 .filter(|(account_id, _relevance)| account_ids.contains(account_id))
138 .collect())
139 }
140
141 async fn check_swap_relevance(
150 &self,
151 note: &Note,
152 account_ids: &BTreeSet<AccountId>,
153 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
154 let note_inputs = note.inputs().values();
155 if note_inputs.len() != 10 {
156 return Err(InvalidNoteInputsError::WrongNumInputs(note.id(), 10).into());
157 }
158
159 let asset_felts: [Felt; 4] = note_inputs[4..8].try_into().expect(
160 "Should be able to convert the second word from note inputs to an array of four Felt elements",
161 );
162
163 let asset: Asset = Word::from(asset_felts)
165 .try_into()
166 .map_err(|err| InvalidNoteInputsError::AssetError(note.id(), err))?;
167
168 let mut accounts_with_relevance = Vec::new();
169
170 for account_id in account_ids {
171 let account: Account = self
172 .store
173 .get_account(*account_id)
174 .await?
175 .ok_or(NoteScreenerError::AccountDataNotFound(*account_id))?
176 .into();
177
178 match asset {
180 Asset::NonFungible(non_fungible_asset)
181 if account.vault().has_non_fungible_asset(non_fungible_asset).expect(
182 "Should be able to query has_non_fungible_asset for an Asset::NonFungible",
183 ) =>
184 {
185 accounts_with_relevance.push((*account_id, NoteRelevance::Always))
186 },
187 Asset::Fungible(fungible_asset) => {
188 let asset_faucet_id = fungible_asset.faucet_id();
189 if account
190 .vault()
191 .get_balance(asset_faucet_id)
192 .expect("Should be able to query get_balance for an Asset::Fungible")
193 >= fungible_asset.amount()
194 {
195 accounts_with_relevance.push((*account_id, NoteRelevance::Always))
196 }
197 },
198 _ => {},
199 }
200 }
201
202 Ok(accounts_with_relevance)
203 }
204
205 fn check_script_relevance(
206 &self,
207 _note: &Note,
208 account_ids: &BTreeSet<AccountId>,
209 ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
210 Ok(account_ids
213 .iter()
214 .map(|account_id| (*account_id, NoteRelevance::Always))
215 .collect())
216 }
217}
218
219#[derive(Debug, Error)]
224pub enum NoteScreenerError {
225 #[error("error while processing note inputs")]
226 InvalidNoteInputsError(#[from] InvalidNoteInputsError),
227 #[error("account data wasn't found for account id {0}")]
228 AccountDataNotFound(AccountId),
229 #[error("error while fetching data from the store")]
230 StoreError(#[from] StoreError),
231}
232
233#[derive(Debug, Error)]
234pub enum InvalidNoteInputsError {
235 #[error("account error for note with id {0}: {1}")]
236 AccountError(NoteId, AccountError),
237 #[error("asset error for note with id {0}: {1}")]
238 AssetError(NoteId, AssetError),
239 #[error("expected {1} note inputs for note with id {0}")]
240 WrongNumInputs(NoteId, usize),
241 #[error("note input representing block with value {1} for note with id {0}")]
242 BlockNumberError(NoteId, u64),
243}