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, SyncedNote};
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 Some(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<(Option<InputNoteRecord>, NoteDetails, BlockNumber, Option<NoteTag>)>,
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 if let Some(tag) = tag {
351 note_requests.push((details.commitment(), *tag));
352 lowest_request_block = lowest_request_block.min(*after_block_num);
353 }
354 }
355 let mut committed_notes_data =
356 self.sync_expected_notes(lowest_request_block, note_requests).await?;
357
358 let mut note_records = vec![];
359 let mut partial_mmr = self.get_current_partial_mmr().await?;
360
361 for (previous_note, details, after_block_num, tag) in requested_notes {
362 let mut note_record = previous_note.unwrap_or_else(|| {
363 InputNoteRecord::new(
364 details,
365 NoteAttachments::empty(),
366 self.store.get_current_timestamp(),
367 ExpectedNoteState { metadata: None, after_block_num, tag }.into(),
368 )
369 });
370
371 let Some(SyncedNote {
373 committed: committed_note, attachments, ..
374 }) = committed_notes_data.remove(¬e_record.details_commitment())
375 else {
376 note_records.push(note_record);
377 continue;
378 };
379
380 let attachments = (!attachments.is_empty()).then_some(attachments);
382
383 let block_header = self
384 .get_and_store_authenticated_block(committed_note.block_num(), &mut partial_mmr)
385 .await?;
386
387 let metadata = *committed_note.metadata();
388 let mut note_changed = note_record
389 .inclusion_proof_received(committed_note.inclusion_proof().clone(), metadata)?;
390
391 if let Some(attachments) = attachments {
392 note_changed |= note_record.attachments_received(attachments);
393 }
394
395 note_changed |= note_record.block_header_received(&block_header)?;
397
398 if note_changed {
400 self.store
401 .remove_note_tag(NoteTagRecord::with_note_source(
402 metadata.tag(),
403 note_record.details_commitment(),
404 ))
405 .await?;
406 }
407
408 if note_changed {
409 note_records.push(note_record);
410 }
411 }
412 self.cache_partial_mmr(partial_mmr).await?;
413
414 Ok(note_records)
415 }
416
417 async fn sync_expected_notes(
425 &mut self,
426 request_block_num: BlockNumber,
427 expected_notes: Vec<(NoteDetailsCommitment, NoteTag)>,
429 ) -> Result<BTreeMap<NoteDetailsCommitment, SyncedNote>, ClientError> {
430 let sync_tags: BTreeSet<NoteTag> = expected_notes.iter().map(|(_, tag)| *tag).collect();
431
432 let mut matched_notes = BTreeMap::new();
433 let current_block_num = self.get_sync_height().await?;
434
435 if request_block_num > current_block_num {
438 return Ok(matched_notes);
439 }
440
441 let blocks = self
442 .rpc_api
443 .sync_notes_with_content(
444 request_block_num,
445 current_block_num,
446 &sync_tags,
447 NoteContentFetch::AttachmentsOnly,
448 )
449 .await
450 .map_err(ClientError::RpcError)?;
451
452 for block in blocks {
453 if block.block_header.block_num() > current_block_num {
454 break;
455 }
456
457 for sync_note in block.notes.into_values() {
458 let committed = &sync_note.committed;
459
460 if committed.block_num() > current_block_num {
465 continue;
466 }
467
468 let Some((commitment, _)) = expected_notes.iter().find(|(commitment, _)| {
469 NoteId::new(*commitment, committed.metadata()) == *committed.note_id()
470 }) else {
471 continue;
472 };
473
474 matched_notes.insert(*commitment, sync_note);
475 }
476 }
477
478 Ok(matched_notes)
479 }
480}
481
482fn ensure_not_processing(previous_note: Option<&InputNoteRecord>) -> Result<(), ClientError> {
488 if let Some(note) = previous_note
489 && note.is_processing()
490 {
491 return Err(ClientError::NoteImportError(format!(
492 "Can't overwrite note with details commitment {} as it's currently being processed",
493 note.details_commitment().to_hex(),
494 )));
495 }
496 Ok(())
497}