1use alloc::string::ToString;
12
13use miden_objects::block::BlockNumber;
14use miden_objects::note::{
15 Note,
16 NoteDetails,
17 NoteFile,
18 NoteId,
19 NoteInclusionProof,
20 NoteMetadata,
21 NoteTag,
22};
23use miden_tx::auth::TransactionAuthenticator;
24
25use crate::rpc::RpcError;
26use crate::rpc::domain::note::FetchedNote;
27use crate::store::input_note_states::ExpectedNoteState;
28use crate::store::{InputNoteRecord, InputNoteState};
29use crate::sync::NoteTagRecord;
30use crate::{Client, ClientError};
31
32impl<AUTH> Client<AUTH>
34where
35 AUTH: TransactionAuthenticator + Sync + 'static,
36{
37 pub async fn import_note(&mut self, note_file: NoteFile) -> Result<NoteId, ClientError> {
56 let id = match ¬e_file {
57 NoteFile::NoteId(id) => *id,
58 NoteFile::NoteDetails { details, .. } => details.id(),
59 NoteFile::NoteWithProof(note, _) => note.id(),
60 };
61
62 let previous_note = self.get_input_note(id).await?;
63
64 if let Some(true) = previous_note.as_ref().map(InputNoteRecord::is_processing) {
66 return Err(ClientError::NoteImportError(format!(
67 "Can't overwrite note with id {id} as it's currently being processed",
68 )));
69 }
70
71 let note = match note_file {
72 NoteFile::NoteId(id) => self.import_note_record_by_id(previous_note, id).await?,
73 NoteFile::NoteDetails { details, after_block_num, tag } => {
74 self.import_note_record_by_details(previous_note, details, after_block_num, tag)
75 .await?
76 },
77 NoteFile::NoteWithProof(note, inclusion_proof) => {
78 self.import_note_record_by_proof(previous_note, note, inclusion_proof).await?
79 },
80 };
81
82 if let Some(note) = note {
83 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) = note.state()
84 {
85 self.store
86 .add_note_tag(NoteTagRecord::with_note_source(*tag, note.id()))
87 .await?;
88 }
89 self.store.upsert_input_notes(&[note]).await?;
90 }
91
92 Ok(id)
93 }
94
95 async fn import_note_record_by_id(
106 &self,
107 previous_note: Option<InputNoteRecord>,
108 id: NoteId,
109 ) -> Result<Option<InputNoteRecord>, ClientError> {
110 let fetched_note = self.rpc_api.get_note_by_id(id).await.map_err(|err| match err {
111 RpcError::NoteNotFound(note_id) => ClientError::NoteNotFoundOnChain(note_id),
112 err => ClientError::RpcError(err),
113 })?;
114
115 let inclusion_proof = fetched_note.inclusion_proof().clone();
116
117 if let Some(mut previous_note) = previous_note {
118 if previous_note.inclusion_proof_received(inclusion_proof, *fetched_note.metadata())? {
119 self.store.remove_note_tag((&previous_note).try_into()?).await?;
120
121 Ok(Some(previous_note))
122 } else {
123 Ok(None)
124 }
125 } else {
126 let fetched_note = match fetched_note {
127 FetchedNote::Public(note, _) => note,
128 FetchedNote::Private(..) => {
129 return Err(ClientError::NoteImportError(
130 "Incomplete imported note is private".to_string(),
131 ));
132 },
133 };
134
135 self.import_note_record_by_proof(previous_note, fetched_note, inclusion_proof)
136 .await
137 }
138 }
139
140 pub(crate) async fn import_note_record_by_proof(
148 &self,
149 previous_note: Option<InputNoteRecord>,
150 note: Note,
151 inclusion_proof: NoteInclusionProof,
152 ) -> Result<Option<InputNoteRecord>, ClientError> {
153 let metadata = *note.metadata();
154 let mut note_record = previous_note.unwrap_or(InputNoteRecord::new(
155 note.into(),
156 self.store.get_current_timestamp(),
157 ExpectedNoteState {
158 metadata: Some(metadata),
159 after_block_num: inclusion_proof.location().block_num(),
160 tag: Some(metadata.tag()),
161 }
162 .into(),
163 ));
164
165 if let Some(block_height) = self
166 .rpc_api
167 .get_nullifier_commit_height(
168 ¬e_record.nullifier(),
169 inclusion_proof.location().block_num(),
170 )
171 .await?
172 {
173 if note_record.consumed_externally(note_record.nullifier(), block_height)? {
174 return Ok(Some(note_record));
175 }
176
177 Ok(None)
178 } else {
179 let block_height = inclusion_proof.location().block_num();
180 let current_block_num = self.get_sync_height().await?;
181
182 let mut note_changed =
183 note_record.inclusion_proof_received(inclusion_proof, metadata)?;
184
185 if block_height <= current_block_num {
186 let mut current_partial_mmr = self.store.get_current_partial_mmr().await?;
189
190 let block_header = self
191 .get_and_store_authenticated_block(block_height, &mut current_partial_mmr)
192 .await?;
193
194 note_changed |= note_record.block_header_received(&block_header)?;
195 } else {
196 self.store
199 .add_note_tag(NoteTagRecord::with_note_source(metadata.tag(), note_record.id()))
200 .await?;
201 }
202
203 if note_changed { Ok(Some(note_record)) } else { Ok(None) }
204 }
205 }
206
207 async fn import_note_record_by_details(
210 &mut self,
211 previous_note: Option<InputNoteRecord>,
212 details: NoteDetails,
213 after_block_num: BlockNumber,
214 tag: Option<NoteTag>,
215 ) -> Result<Option<InputNoteRecord>, ClientError> {
216 let mut note_record = previous_note.unwrap_or({
217 InputNoteRecord::new(
218 details,
219 self.store.get_current_timestamp(),
220 ExpectedNoteState { metadata: None, after_block_num, tag }.into(),
221 )
222 });
223
224 let committed_note_data = if let Some(tag) = tag {
225 self.check_expected_note(after_block_num, tag, note_record.details()).await?
226 } else {
227 None
228 };
229
230 match committed_note_data {
231 Some((metadata, inclusion_proof)) => {
232 let mut current_partial_mmr = self.store.get_current_partial_mmr().await?;
233 let block_header = self
234 .get_and_store_authenticated_block(
235 inclusion_proof.location().block_num(),
236 &mut current_partial_mmr,
237 )
238 .await?;
239
240 let note_changed =
241 note_record.inclusion_proof_received(inclusion_proof, metadata)?;
242
243 if note_record.block_header_received(&block_header)? | note_changed {
244 self.store
245 .remove_note_tag(NoteTagRecord::with_note_source(
246 metadata.tag(),
247 note_record.id(),
248 ))
249 .await?;
250
251 Ok(Some(note_record))
252 } else {
253 Ok(None)
254 }
255 },
256 None => Ok(Some(note_record)),
257 }
258 }
259
260 async fn check_expected_note(
264 &mut self,
265 mut request_block_num: BlockNumber,
266 tag: NoteTag,
267 expected_note: &miden_objects::note::NoteDetails,
268 ) -> Result<Option<(NoteMetadata, NoteInclusionProof)>, ClientError> {
269 let current_block_num = self.get_sync_height().await?;
270 loop {
271 if request_block_num > current_block_num {
272 return Ok(None);
273 }
274
275 let sync_notes = self
276 .rpc_api
277 .sync_notes(request_block_num, None, &[tag].into_iter().collect())
278 .await?;
279
280 let committed_note =
283 sync_notes.notes.iter().find(|note| note.note_id() == &expected_note.id());
284
285 if let Some(note) = committed_note {
286 let note_block_num = sync_notes.block_header.block_num();
289
290 if note_block_num > current_block_num {
291 return Ok(None);
292 }
293
294 let note_inclusion_proof = NoteInclusionProof::new(
295 note_block_num,
296 note.note_index(),
297 note.inclusion_path().clone(),
298 )?;
299
300 return Ok(Some((note.metadata(), note_inclusion_proof)));
301 }
302
303 if sync_notes.block_header.block_num() == sync_notes.chain_tip {
305 return Ok(None);
306 }
307
308 request_block_num = sync_notes.block_header.block_num();
313 }
314 }
315}