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 NoteTag,
24};
25use miden_standards::note::NoteFile;
26use miden_tx::auth::TransactionAuthenticator;
27
28use crate::rpc::domain::note::{FetchedNote, ResolvedSyncNotesBlock};
29use crate::rpc::{NoteContentFetch, RpcError};
30use crate::store::input_note_states::ExpectedNoteState;
31use crate::store::{InputNoteRecord, InputNoteState, NoteFilter};
32use crate::sync::NoteTagRecord;
33use crate::{Client, ClientError};
34
35impl<AUTH> Client<AUTH>
37where
38 AUTH: TransactionAuthenticator + Sync + 'static,
39{
40 pub async fn import_notes(
68 &mut self,
69 note_files: &[NoteFile],
70 ) -> Result<Vec<NoteDetailsCommitment>, ClientError> {
71 self.ensure_genesis_in_place().await?;
72
73 let mut ids = BTreeSet::new();
78 let mut files_by_commitment = BTreeMap::new();
79 for note_file in note_files {
80 match note_file {
81 NoteFile::NoteId(id) => {
82 ids.insert(*id);
83 },
84 NoteFile::ExpectedNote { details, .. } => {
85 files_by_commitment.insert(details.commitment(), note_file.clone());
86 },
87 NoteFile::Committed { note, .. } => {
88 files_by_commitment.insert(note.details_commitment(), note_file.clone());
89 },
90 }
91 }
92
93 let previous_by_id: BTreeMap<NoteId, InputNoteRecord> = self
96 .get_input_notes(NoteFilter::List(ids.iter().copied().collect()))
97 .await?
98 .into_iter()
99 .filter_map(|note| note.id().map(|id| (id, note)))
100 .collect();
101 let previous_by_commitment: BTreeMap<NoteDetailsCommitment, InputNoteRecord> = self
102 .get_input_notes(NoteFilter::DetailsCommitments(
103 files_by_commitment.keys().copied().collect(),
104 ))
105 .await?
106 .into_iter()
107 .map(|note| (note.details_commitment(), note))
108 .collect();
109
110 let mut requests_by_id = BTreeMap::new();
113 let mut requests_by_details = vec![];
114 let mut requests_by_proof = vec![];
115
116 for id in ids {
117 let previous_note = previous_by_id.get(&id).cloned();
118 ensure_not_processing(previous_note.as_ref())?;
119 requests_by_id.insert(id, previous_note);
120 }
121
122 for (commitment, note_file) in files_by_commitment {
123 let previous_note = previous_by_commitment.get(&commitment).cloned();
124 ensure_not_processing(previous_note.as_ref())?;
125 match note_file {
126 NoteFile::ExpectedNote { details, sync_hint } => {
127 requests_by_details.push((
128 previous_note,
129 details,
130 sync_hint.after_block_num(),
131 sync_hint.tag(),
132 ));
133 },
134 NoteFile::Committed { note, proof } => {
135 requests_by_proof.push((previous_note, note, proof));
136 },
137 NoteFile::NoteId(_) => {
138 unreachable!("files_by_commitment only holds detail-carrying note files")
139 },
140 }
141 }
142
143 let mut imported_notes = vec![];
144 if !requests_by_id.is_empty() {
145 let notes_by_id = self.import_note_records_by_id(requests_by_id).await?;
146 imported_notes.extend(notes_by_id);
147 }
148
149 if !requests_by_details.is_empty() {
150 let notes_by_details = self.import_note_records_by_details(requests_by_details).await?;
151 imported_notes.extend(notes_by_details);
152 }
153
154 if !requests_by_proof.is_empty() {
155 let notes_by_proof = self.import_note_records_by_proof(requests_by_proof).await?;
156 imported_notes.extend(notes_by_proof);
157 }
158
159 let mut imported_commitments = Vec::with_capacity(imported_notes.len());
160 for note in imported_notes {
161 let details_commitment = note.details_commitment();
162 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) = note.state()
163 {
164 self.store
165 .add_note_tag(NoteTagRecord::with_note_source(*tag, details_commitment))
166 .await?;
167 }
168 self.store.upsert_input_notes(&[note]).await?;
169 imported_commitments.push(details_commitment);
170 }
171
172 Ok(imported_commitments)
173 }
174
175 async fn import_note_records_by_id(
188 &mut self,
189 notes: BTreeMap<NoteId, Option<InputNoteRecord>>,
190 ) -> Result<Vec<InputNoteRecord>, ClientError> {
191 let note_ids = notes.keys().copied().collect::<Vec<_>>();
192
193 let fetched_notes =
194 self.rpc_api.get_notes_by_id(¬e_ids).await.map_err(|err| match err {
195 RpcError::NoteNotFound(note_id) => ClientError::NoteNotFoundOnChain(note_id),
196 err => ClientError::RpcError(err),
197 })?;
198
199 if fetched_notes.is_empty() {
200 return Err(ClientError::NoteImportError("No notes fetched from node".to_string()));
201 }
202
203 let mut note_records = Vec::new();
204 let mut notes_to_request = vec![];
205 for fetched_note in fetched_notes {
206 let note_id = fetched_note.id();
207 let inclusion_proof = fetched_note.inclusion_proof().clone();
208
209 let previous_note =
210 notes.get(¬e_id).cloned().ok_or(ClientError::NoteImportError(format!(
211 "Failed to retrieve note with id {note_id} from node"
212 )))?;
213 if let Some(mut previous_note) = previous_note {
214 if previous_note
215 .inclusion_proof_received(inclusion_proof, *fetched_note.metadata())?
216 {
217 self.store.remove_note_tag((&previous_note).try_into()?).await?;
218
219 note_records.push(previous_note);
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(
253 &mut self,
254 requested_notes: Vec<(Option<InputNoteRecord>, Note, NoteInclusionProof)>,
255 ) -> Result<Vec<InputNoteRecord>, ClientError> {
256 let mut note_records = vec![];
258
259 let mut nullifier_requests = BTreeSet::new();
260 let mut lowest_block_height: BlockNumber = u32::MAX.into();
261 for (previous_note, note, inclusion_proof) in &requested_notes {
262 let nullifier = match previous_note {
263 Some(previous_note) => previous_note.nullifier(),
264 None => Some(note.nullifier()),
265 };
266 if let Some(nullifier) = nullifier {
267 nullifier_requests.insert(nullifier);
268 }
269 if inclusion_proof.location().block_num() < lowest_block_height {
270 lowest_block_height = inclusion_proof.location().block_num();
271 }
272 }
273
274 let nullifier_commit_heights = self
275 .rpc_api
276 .get_nullifier_commit_heights(nullifier_requests, lowest_block_height)
277 .await?;
278 let mut partial_mmr = self.get_current_partial_mmr().await?;
279
280 for (previous_note, note, inclusion_proof) in requested_notes {
281 let metadata = *note.metadata();
282 let attachments = note.attachments().clone();
283 let mut note_record = previous_note.unwrap_or(InputNoteRecord::new(
284 note.into(),
285 attachments,
286 self.store.get_current_timestamp(),
287 ExpectedNoteState {
288 metadata: Some(metadata),
289 after_block_num: inclusion_proof.location().block_num(),
290 tag: Some(metadata.tag()),
291 }
292 .into(),
293 ));
294
295 if let Some(nullifier) = note_record.nullifier()
296 && let Some(Some(block_height)) = nullifier_commit_heights.get(&nullifier)
297 {
298 if note_record.consumed_externally(nullifier, *block_height, None)? {
299 note_records.push(note_record);
300 }
301 } else {
302 let block_height = inclusion_proof.location().block_num();
303 let current_block_num = self.get_sync_height().await?;
304
305 let tag = metadata.tag();
306 let mut note_changed =
307 note_record.inclusion_proof_received(inclusion_proof, metadata)?;
308
309 if block_height <= current_block_num {
310 let block_header = self
313 .get_and_store_authenticated_block(block_height, &mut partial_mmr)
314 .await?;
315 note_changed |= note_record.block_header_received(&block_header)?;
316 } else {
317 self.store
320 .add_note_tag(NoteTagRecord::with_note_source(
321 tag,
322 note_record.details_commitment(),
323 ))
324 .await?;
325 }
326
327 if note_changed {
328 note_records.push(note_record);
329 }
330 }
331 }
332 self.cache_partial_mmr(partial_mmr).await?;
333
334 Ok(note_records)
335 }
336
337 async fn import_note_records_by_details(
344 &mut self,
345 requested_notes: Vec<NoteImportByDetailsRequest>,
346 ) -> Result<Vec<InputNoteRecord>, ClientError> {
347 let mut lowest_request_block: BlockNumber = u32::MAX.into();
348 let mut note_requests = vec![];
349 for (_, details, after_block_num, tag) in &requested_notes {
350 note_requests.push((details.commitment(), *tag));
351 lowest_request_block = lowest_request_block.min(*after_block_num);
352 }
353 let blocks = self.sync_expected_notes(lowest_request_block, ¬e_requests).await?;
354
355 let mut partial_mmr = self.get_current_partial_mmr().await?;
359 self.insert_note_blocks(&blocks, &mut partial_mmr).await?;
360 self.cache_partial_mmr(partial_mmr).await?;
361
362 let mut note_records = vec![];
363 for (previous_note, details, after_block_num, tag) in requested_notes {
364 let mut note_record = previous_note.unwrap_or_else(|| {
365 InputNoteRecord::new(
366 details,
367 NoteAttachments::empty(),
368 self.store.get_current_timestamp(),
369 ExpectedNoteState {
370 metadata: None,
371 after_block_num,
372 tag: Some(tag),
373 }
374 .into(),
375 )
376 });
377
378 let commitment = note_record.details_commitment();
380 let Some((sync_note, block_header)) = blocks.iter().find_map(|block| {
381 let sync_note = block.notes.values().find(|sync_note| {
382 NoteId::new(commitment, &sync_note.metadata) == sync_note.note_id
383 })?;
384 Some((sync_note, &block.block_header))
385 }) else {
386 note_records.push(note_record);
387 continue;
388 };
389
390 let attachments =
392 (!sync_note.attachments.is_empty()).then(|| sync_note.attachments.clone());
393
394 let metadata = sync_note.metadata;
395 let mut note_changed = note_record
396 .inclusion_proof_received(sync_note.inclusion_proof.clone(), metadata)?;
397
398 if let Some(attachments) = attachments {
399 note_changed |= note_record.attachments_received(attachments);
400 }
401
402 note_changed |= note_record.block_header_received(block_header)?;
404
405 if note_changed {
407 self.store
408 .remove_note_tag(NoteTagRecord::with_note_source(
409 metadata.tag(),
410 note_record.details_commitment(),
411 ))
412 .await?;
413 }
414
415 if note_changed {
416 note_records.push(note_record);
417 }
418 }
419
420 self.mark_externally_consumed(&mut note_records).await?;
421
422 Ok(note_records)
423 }
424
425 async fn mark_externally_consumed(
431 &self,
432 note_records: &mut [InputNoteRecord],
433 ) -> Result<(), ClientError> {
434 let mut nullifiers = BTreeSet::new();
435 let mut lowest_commitment_block: BlockNumber = u32::MAX.into();
436 for note_record in note_records.iter() {
437 let (Some(nullifier), Some(inclusion_proof)) =
438 (note_record.nullifier(), note_record.inclusion_proof())
439 else {
440 continue;
441 };
442 nullifiers.insert(nullifier);
443 lowest_commitment_block =
444 lowest_commitment_block.min(inclusion_proof.location().block_num());
445 }
446
447 if nullifiers.is_empty() {
448 return Ok(());
449 }
450
451 let spent_heights = self
452 .rpc_api
453 .get_nullifier_commit_heights(nullifiers, lowest_commitment_block)
454 .await?;
455
456 let sync_height = self.get_sync_height().await?;
457 for note_record in note_records.iter_mut() {
458 let Some(nullifier) = note_record.nullifier() else {
459 continue;
460 };
461 if let Some(Some(spent_at)) = spent_heights.get(&nullifier)
462 && *spent_at <= sync_height
463 {
464 note_record.consumed_externally(nullifier, *spent_at, None)?;
465 }
466 }
467
468 Ok(())
469 }
470
471 async fn sync_expected_notes(
478 &self,
479 request_block_num: BlockNumber,
480 expected_notes: &[(NoteDetailsCommitment, NoteTag)],
482 ) -> Result<Vec<ResolvedSyncNotesBlock>, ClientError> {
483 let sync_tags: BTreeSet<NoteTag> = expected_notes.iter().map(|(_, tag)| *tag).collect();
484 let current_block_num = self.get_sync_height().await?;
485
486 if request_block_num > current_block_num {
489 return Ok(Vec::new());
490 }
491
492 let blocks = self
493 .rpc_api
494 .sync_notes_with_content(
495 request_block_num,
496 current_block_num,
497 &sync_tags,
498 NoteContentFetch::AttachmentsOnly,
499 )
500 .await
501 .map_err(ClientError::RpcError)?;
502
503 let mut matched_blocks = vec![];
504 for block in blocks {
505 let mut block_matches = false;
506 if block.block_header.block_num() > current_block_num {
507 break;
508 }
509
510 for sync_note in block.notes.values() {
511 if sync_note.block_num() > current_block_num {
516 continue;
517 }
518
519 let Some((..)) = expected_notes.iter().find(|(commitment, _)| {
520 NoteId::new(*commitment, &sync_note.metadata) == sync_note.note_id
521 }) else {
522 continue;
523 };
524
525 block_matches = true;
526 }
527
528 if block_matches {
529 matched_blocks.push(block);
530 }
531 }
532
533 Ok(matched_blocks)
534 }
535}
536
537pub(crate) type NoteImportByDetailsRequest =
540 (Option<InputNoteRecord>, NoteDetails, BlockNumber, NoteTag);
541
542pub fn ensure_not_processing(previous_note: Option<&InputNoteRecord>) -> Result<(), ClientError> {
548 if let Some(note) = previous_note
549 && note.is_processing()
550 {
551 return Err(ClientError::NoteImportError(format!(
552 "Can't overwrite note with details commitment {} as it's currently being processed",
553 note.details_commitment().to_hex(),
554 )));
555 }
556 Ok(())
557}