1use alloc::collections::{BTreeMap, BTreeSet};
12use alloc::string::ToString;
13use alloc::vec::Vec;
14
15use miden_protocol::block::BlockNumber;
16use miden_protocol::note::{
17 Note,
18 NoteAttachments,
19 NoteDetails,
20 NoteDetailsCommitment,
21 NoteId,
22 NoteInclusionProof,
23 NoteMetadata,
24 NoteTag,
25};
26use miden_standards::note::NoteFile;
27use miden_tx::auth::TransactionAuthenticator;
28
29use crate::rpc::RpcError;
30use crate::rpc::domain::note::FetchedNote;
31use crate::store::input_note_states::ExpectedNoteState;
32use crate::store::{InputNoteRecord, InputNoteState, NoteFilter};
33use crate::sync::NoteTagRecord;
34use crate::{Client, ClientError};
35
36impl<AUTH> Client<AUTH>
38where
39 AUTH: TransactionAuthenticator + Sync + 'static,
40{
41 pub async fn import_notes(
67 &mut self,
68 note_files: &[NoteFile],
69 ) -> Result<Vec<NoteDetailsCommitment>, ClientError> {
70 let mut ids = BTreeSet::new();
75 let mut files_by_commitment = BTreeMap::new();
76 for note_file in note_files {
77 match note_file {
78 NoteFile::NoteId(id) => {
79 ids.insert(*id);
80 },
81 NoteFile::ExpectedNote { details, .. } => {
82 files_by_commitment.insert(details.commitment(), note_file.clone());
83 },
84 NoteFile::Committed { note, .. } => {
85 files_by_commitment.insert(note.details_commitment(), note_file.clone());
86 },
87 }
88 }
89
90 let previous_by_id: BTreeMap<NoteId, InputNoteRecord> = self
93 .get_input_notes(NoteFilter::List(ids.iter().copied().collect()))
94 .await?
95 .into_iter()
96 .filter_map(|note| note.id().map(|id| (id, note)))
97 .collect();
98 let previous_by_commitment: BTreeMap<NoteDetailsCommitment, InputNoteRecord> = self
99 .get_input_notes(NoteFilter::DetailsCommitments(
100 files_by_commitment.keys().copied().collect(),
101 ))
102 .await?
103 .into_iter()
104 .map(|note| (note.details_commitment(), note))
105 .collect();
106
107 let mut requests_by_id = BTreeMap::new();
110 let mut requests_by_details = vec![];
111 let mut requests_by_proof = vec![];
112
113 for id in ids {
114 let previous_note = previous_by_id.get(&id).cloned();
115 ensure_not_processing(previous_note.as_ref())?;
116 requests_by_id.insert(id, previous_note);
117 }
118
119 for (commitment, note_file) in files_by_commitment {
120 let previous_note = previous_by_commitment.get(&commitment).cloned();
121 ensure_not_processing(previous_note.as_ref())?;
122 match note_file {
123 NoteFile::ExpectedNote { details, sync_hint } => {
124 requests_by_details.push((
125 previous_note,
126 details,
127 sync_hint.after_block_num(),
128 Some(sync_hint.tag()),
129 ));
130 },
131 NoteFile::Committed { note, proof } => {
132 requests_by_proof.push((previous_note, note, proof));
133 },
134 NoteFile::NoteId(_) => {
135 unreachable!("files_by_commitment only holds detail-carrying note files")
136 },
137 }
138 }
139
140 let mut imported_notes = vec![];
141 if !requests_by_id.is_empty() {
142 let notes_by_id = self.import_note_records_by_id(requests_by_id).await?;
143 imported_notes.extend(notes_by_id);
144 }
145
146 if !requests_by_details.is_empty() {
147 let notes_by_details = self.import_note_records_by_details(requests_by_details).await?;
148 imported_notes.extend(notes_by_details);
149 }
150
151 if !requests_by_proof.is_empty() {
152 let notes_by_proof = self.import_note_records_by_proof(requests_by_proof).await?;
153 imported_notes.extend(notes_by_proof);
154 }
155
156 let mut imported_commitments = Vec::with_capacity(imported_notes.len());
157 for note in imported_notes.into_iter().flatten() {
158 let details_commitment = note.details_commitment();
159 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) = note.state()
160 {
161 self.store
162 .add_note_tag(NoteTagRecord::with_note_source(*tag, details_commitment))
163 .await?;
164 }
165 self.store.upsert_input_notes(&[note]).await?;
166 imported_commitments.push(details_commitment);
167 }
168
169 Ok(imported_commitments)
170 }
171
172 async fn import_note_records_by_id(
186 &mut self,
187 notes: BTreeMap<NoteId, Option<InputNoteRecord>>,
188 ) -> Result<Vec<Option<InputNoteRecord>>, ClientError> {
189 let note_ids = notes.keys().copied().collect::<Vec<_>>();
190
191 let fetched_notes =
192 self.rpc_api.get_notes_by_id(¬e_ids).await.map_err(|err| match err {
193 RpcError::NoteNotFound(note_id) => ClientError::NoteNotFoundOnChain(note_id),
194 err => ClientError::RpcError(err),
195 })?;
196
197 if fetched_notes.is_empty() {
198 return Err(ClientError::NoteImportError("No notes fetched from node".to_string()));
199 }
200
201 let mut note_records = Vec::new();
202 let mut notes_to_request = vec![];
203 for fetched_note in fetched_notes {
204 let note_id = fetched_note.id();
205 let inclusion_proof = fetched_note.inclusion_proof().clone();
206
207 let previous_note =
208 notes.get(¬e_id).cloned().ok_or(ClientError::NoteImportError(format!(
209 "Failed to retrieve note with id {note_id} from node"
210 )))?;
211 if let Some(mut previous_note) = previous_note {
212 if previous_note
213 .inclusion_proof_received(inclusion_proof, *fetched_note.metadata())?
214 {
215 self.store.remove_note_tag((&previous_note).try_into()?).await?;
216
217 note_records.push(Some(previous_note));
218 } else {
219 note_records.push(None);
220 }
221 } else {
222 let fetched_note = match fetched_note {
223 FetchedNote::Public(note, _) => note,
224 FetchedNote::Private(..) => {
225 return Err(ClientError::NoteImportError(
226 "Incomplete imported note is private".to_string(),
227 ));
228 },
229 };
230
231 let note_request = (previous_note, fetched_note, inclusion_proof);
232 notes_to_request.push(note_request);
233 }
234 }
235
236 if !notes_to_request.is_empty() {
237 let note_records_by_proof = self.import_note_records_by_proof(notes_to_request).await?;
238 note_records.extend(note_records_by_proof);
239 }
240 Ok(note_records)
241 }
242
243 pub(crate) async fn import_note_records_by_proof(
251 &mut self,
252 requested_notes: Vec<(Option<InputNoteRecord>, Note, NoteInclusionProof)>,
253 ) -> Result<Vec<Option<InputNoteRecord>>, ClientError> {
254 let mut note_records = vec![];
256
257 let mut nullifier_requests = BTreeSet::new();
258 let mut lowest_block_height: BlockNumber = u32::MAX.into();
259 for (previous_note, note, inclusion_proof) in &requested_notes {
260 let nullifier = match previous_note {
261 Some(previous_note) => previous_note.nullifier(),
262 None => Some(note.nullifier()),
263 };
264 if let Some(nullifier) = nullifier {
265 nullifier_requests.insert(nullifier);
266 }
267 if inclusion_proof.location().block_num() < lowest_block_height {
268 lowest_block_height = inclusion_proof.location().block_num();
269 }
270 }
271
272 let nullifier_commit_heights = self
273 .rpc_api
274 .get_nullifier_commit_heights(nullifier_requests, lowest_block_height)
275 .await?;
276
277 for (previous_note, note, inclusion_proof) in requested_notes {
278 let metadata = *note.metadata();
279 let attachments = note.attachments().clone();
280 let mut note_record = previous_note.unwrap_or(InputNoteRecord::new(
281 note.into(),
282 attachments,
283 self.store.get_current_timestamp(),
284 ExpectedNoteState {
285 metadata: Some(metadata),
286 after_block_num: inclusion_proof.location().block_num(),
287 tag: Some(metadata.tag()),
288 }
289 .into(),
290 ));
291
292 if let Some(nullifier) = note_record.nullifier()
293 && let Some(Some(block_height)) = nullifier_commit_heights.get(&nullifier)
294 {
295 if note_record.consumed_externally(nullifier, *block_height, None)? {
296 note_records.push(Some(note_record));
297 }
298
299 note_records.push(None);
300 } else {
301 let block_height = inclusion_proof.location().block_num();
302 let current_block_num = self.get_sync_height().await?;
303
304 let tag = metadata.tag();
305 let mut note_changed =
306 note_record.inclusion_proof_received(inclusion_proof, metadata)?;
307
308 if block_height <= current_block_num {
309 let mut partial_mmr = self.get_current_partial_mmr().await?;
315 let block_header = self
316 .get_and_store_authenticated_block(block_height, &mut partial_mmr)
317 .await?;
318 self.cache_partial_mmr(partial_mmr).await?;
319
320 note_changed |= note_record.block_header_received(&block_header)?;
321 } else {
322 self.store
325 .add_note_tag(NoteTagRecord::with_note_source(
326 tag,
327 note_record.details_commitment(),
328 ))
329 .await?;
330 }
331
332 if note_changed {
333 note_records.push(Some(note_record));
334 } else {
335 note_records.push(None);
336 }
337 }
338 }
339
340 Ok(note_records)
341 }
342
343 async fn import_note_records_by_details(
346 &mut self,
347 requested_notes: Vec<(Option<InputNoteRecord>, NoteDetails, BlockNumber, Option<NoteTag>)>,
348 ) -> Result<Vec<Option<InputNoteRecord>>, ClientError> {
349 let mut lowest_request_block: BlockNumber = u32::MAX.into();
350 let mut note_requests = vec![];
351 for (_, details, after_block_num, tag) in &requested_notes {
352 if let Some(tag) = tag {
353 note_requests.push((details.commitment(), tag));
354 if after_block_num < &lowest_request_block {
355 lowest_request_block = *after_block_num;
356 }
357 }
358 }
359 let mut committed_notes_data =
360 self.check_expected_notes(lowest_request_block, note_requests).await?;
361
362 let mut note_records = vec![];
363 for (previous_note, details, after_block_num, tag) in requested_notes {
364 let note_record = previous_note.unwrap_or({
365 InputNoteRecord::new(
366 details,
367 NoteAttachments::empty(),
368 self.store.get_current_timestamp(),
369 ExpectedNoteState { metadata: None, after_block_num, tag }.into(),
370 )
371 });
372
373 match committed_notes_data.remove(¬e_record.details_commitment()) {
374 Some((metadata, inclusion_proof)) => {
375 let mut partial_mmr = self.get_current_partial_mmr().await?;
378 let block_header = self
379 .get_and_store_authenticated_block(
380 inclusion_proof.location().block_num(),
381 &mut partial_mmr,
382 )
383 .await?;
384
385 self.cache_partial_mmr(partial_mmr).await?;
386
387 let tag = metadata.tag();
388 let mut note_record = note_record;
389 let note_changed =
390 note_record.inclusion_proof_received(inclusion_proof, metadata)?;
391
392 if note_record.block_header_received(&block_header)? | note_changed {
393 self.store
394 .remove_note_tag(NoteTagRecord::with_note_source(
395 tag,
396 note_record.details_commitment(),
397 ))
398 .await?;
399
400 note_records.push(Some(note_record));
401 } else {
402 note_records.push(None);
403 }
404 },
405 None => {
406 note_records.push(Some(note_record));
407 },
408 }
409 }
410
411 Ok(note_records)
412 }
413
414 async fn check_expected_notes(
422 &mut self,
423 request_block_num: BlockNumber,
424 expected_notes: Vec<(NoteDetailsCommitment, &NoteTag)>,
426 ) -> Result<BTreeMap<NoteDetailsCommitment, (NoteMetadata, NoteInclusionProof)>, ClientError>
427 {
428 let tracked_tags: BTreeSet<NoteTag> = expected_notes.iter().map(|(_, tag)| **tag).collect();
429 let mut retrieved_proofs = BTreeMap::new();
430 let current_block_num = self.get_sync_height().await?;
431
432 if request_block_num > current_block_num {
433 return Ok(retrieved_proofs);
434 }
435
436 let blocks = self
437 .rpc_api
438 .sync_notes(request_block_num, current_block_num, &tracked_tags)
439 .await
440 .map_err(ClientError::RpcError)?;
441
442 for block in &blocks {
443 if block.block_header.block_num() > current_block_num {
444 break;
445 }
446
447 for sync_note in block.notes.values() {
448 let Some((commitment, _)) = expected_notes.iter().find(|(commitment, _)| {
449 NoteId::new(*commitment, sync_note.metadata()) == *sync_note.note_id()
450 }) else {
451 continue;
452 };
453
454 retrieved_proofs.insert(
455 *commitment,
456 (*sync_note.metadata(), sync_note.inclusion_proof().clone()),
457 );
458 }
459 }
460
461 Ok(retrieved_proofs)
462 }
463}
464
465fn ensure_not_processing(previous_note: Option<&InputNoteRecord>) -> Result<(), ClientError> {
471 if let Some(note) = previous_note
472 && note.is_processing()
473 {
474 return Err(ClientError::NoteImportError(format!(
475 "Can't overwrite note with details commitment {} as it's currently being processed",
476 note.details_commitment().to_hex(),
477 )));
478 }
479 Ok(())
480}