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 NoteFile,
22 NoteId,
23 NoteInclusionProof,
24 NoteMetadata,
25 NoteTag,
26};
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::NoteDetails { details, .. } => {
82 files_by_commitment.insert(details.commitment(), note_file.clone());
83 },
84 NoteFile::NoteWithProof(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::NoteDetails { details, after_block_num, tag } => {
124 requests_by_details.push((previous_note, details, after_block_num, tag));
125 },
126 NoteFile::NoteWithProof(note, inclusion_proof) => {
127 requests_by_proof.push((previous_note, note, inclusion_proof));
128 },
129 NoteFile::NoteId(_) => {
130 unreachable!("files_by_commitment only holds detail-carrying note files")
131 },
132 }
133 }
134
135 let mut imported_notes = vec![];
136 if !requests_by_id.is_empty() {
137 let notes_by_id = self.import_note_records_by_id(requests_by_id).await?;
138 imported_notes.extend(notes_by_id);
139 }
140
141 if !requests_by_details.is_empty() {
142 let notes_by_details = self.import_note_records_by_details(requests_by_details).await?;
143 imported_notes.extend(notes_by_details);
144 }
145
146 if !requests_by_proof.is_empty() {
147 let notes_by_proof = self.import_note_records_by_proof(requests_by_proof).await?;
148 imported_notes.extend(notes_by_proof);
149 }
150
151 let mut imported_commitments = Vec::with_capacity(imported_notes.len());
152 for note in imported_notes.into_iter().flatten() {
153 let details_commitment = note.details_commitment();
154 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) = note.state()
155 {
156 self.store
157 .add_note_tag(NoteTagRecord::with_note_source(*tag, details_commitment))
158 .await?;
159 }
160 self.store.upsert_input_notes(&[note]).await?;
161 imported_commitments.push(details_commitment);
162 }
163
164 Ok(imported_commitments)
165 }
166
167 async fn import_note_records_by_id(
181 &mut self,
182 notes: BTreeMap<NoteId, Option<InputNoteRecord>>,
183 ) -> Result<Vec<Option<InputNoteRecord>>, ClientError> {
184 let note_ids = notes.keys().copied().collect::<Vec<_>>();
185
186 let fetched_notes =
187 self.rpc_api.get_notes_by_id(¬e_ids).await.map_err(|err| match err {
188 RpcError::NoteNotFound(note_id) => ClientError::NoteNotFoundOnChain(note_id),
189 err => ClientError::RpcError(err),
190 })?;
191
192 if fetched_notes.is_empty() {
193 return Err(ClientError::NoteImportError("No notes fetched from node".to_string()));
194 }
195
196 let mut note_records = Vec::new();
197 let mut notes_to_request = vec![];
198 for fetched_note in fetched_notes {
199 let note_id = fetched_note.id();
200 let inclusion_proof = fetched_note.inclusion_proof().clone();
201
202 let previous_note =
203 notes.get(¬e_id).cloned().ok_or(ClientError::NoteImportError(format!(
204 "Failed to retrieve note with id {note_id} from node"
205 )))?;
206 if let Some(mut previous_note) = previous_note {
207 if previous_note
208 .inclusion_proof_received(inclusion_proof, *fetched_note.metadata())?
209 {
210 self.store.remove_note_tag((&previous_note).try_into()?).await?;
211
212 note_records.push(Some(previous_note));
213 } else {
214 note_records.push(None);
215 }
216 } else {
217 let fetched_note = match fetched_note {
218 FetchedNote::Public(note, _) => note,
219 FetchedNote::Private(..) => {
220 return Err(ClientError::NoteImportError(
221 "Incomplete imported note is private".to_string(),
222 ));
223 },
224 };
225
226 let note_request = (previous_note, fetched_note, inclusion_proof);
227 notes_to_request.push(note_request);
228 }
229 }
230
231 if !notes_to_request.is_empty() {
232 let note_records_by_proof = self.import_note_records_by_proof(notes_to_request).await?;
233 note_records.extend(note_records_by_proof);
234 }
235 Ok(note_records)
236 }
237
238 pub(crate) async fn import_note_records_by_proof(
246 &mut self,
247 requested_notes: Vec<(Option<InputNoteRecord>, Note, NoteInclusionProof)>,
248 ) -> Result<Vec<Option<InputNoteRecord>>, ClientError> {
249 let mut note_records = vec![];
251
252 let mut nullifier_requests = BTreeSet::new();
253 let mut lowest_block_height: BlockNumber = u32::MAX.into();
254 for (previous_note, note, inclusion_proof) in &requested_notes {
255 let nullifier = match previous_note {
256 Some(previous_note) => previous_note.nullifier(),
257 None => Some(note.nullifier()),
258 };
259 if let Some(nullifier) = nullifier {
260 nullifier_requests.insert(nullifier);
261 }
262 if inclusion_proof.location().block_num() < lowest_block_height {
263 lowest_block_height = inclusion_proof.location().block_num();
264 }
265 }
266
267 let nullifier_commit_heights = self
268 .rpc_api
269 .get_nullifier_commit_heights(nullifier_requests, lowest_block_height)
270 .await?;
271
272 for (previous_note, note, inclusion_proof) in requested_notes {
273 let metadata = *note.metadata();
274 let attachments = note.attachments().clone();
275 let mut note_record = previous_note.unwrap_or(InputNoteRecord::new(
276 note.into(),
277 attachments,
278 self.store.get_current_timestamp(),
279 ExpectedNoteState {
280 metadata: Some(metadata),
281 after_block_num: inclusion_proof.location().block_num(),
282 tag: Some(metadata.tag()),
283 }
284 .into(),
285 ));
286
287 if let Some(nullifier) = note_record.nullifier()
288 && let Some(Some(block_height)) = nullifier_commit_heights.get(&nullifier)
289 {
290 if note_record.consumed_externally(nullifier, *block_height, None)? {
291 note_records.push(Some(note_record));
292 }
293
294 note_records.push(None);
295 } else {
296 let block_height = inclusion_proof.location().block_num();
297 let current_block_num = self.get_sync_height().await?;
298
299 let tag = metadata.tag();
300 let mut note_changed =
301 note_record.inclusion_proof_received(inclusion_proof, metadata)?;
302
303 if block_height <= current_block_num {
304 let mut partial_mmr = self.get_current_partial_mmr().await?;
310 let block_header = self
311 .get_and_store_authenticated_block(block_height, &mut partial_mmr)
312 .await?;
313 self.cache_partial_mmr(partial_mmr).await?;
314
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(Some(note_record));
329 } else {
330 note_records.push(None);
331 }
332 }
333 }
334
335 Ok(note_records)
336 }
337
338 async fn import_note_records_by_details(
341 &mut self,
342 requested_notes: Vec<(Option<InputNoteRecord>, NoteDetails, BlockNumber, Option<NoteTag>)>,
343 ) -> Result<Vec<Option<InputNoteRecord>>, ClientError> {
344 let mut lowest_request_block: BlockNumber = u32::MAX.into();
345 let mut note_requests = vec![];
346 for (_, details, after_block_num, tag) in &requested_notes {
347 if let Some(tag) = tag {
348 note_requests.push((details.commitment(), tag));
349 if after_block_num < &lowest_request_block {
350 lowest_request_block = *after_block_num;
351 }
352 }
353 }
354 let mut committed_notes_data =
355 self.check_expected_notes(lowest_request_block, note_requests).await?;
356
357 let mut note_records = vec![];
358 for (previous_note, details, after_block_num, tag) in requested_notes {
359 let note_record = previous_note.unwrap_or({
360 InputNoteRecord::new(
361 details,
362 NoteAttachments::empty(),
363 self.store.get_current_timestamp(),
364 ExpectedNoteState { metadata: None, after_block_num, tag }.into(),
365 )
366 });
367
368 match committed_notes_data.remove(¬e_record.details_commitment()) {
369 Some((metadata, inclusion_proof)) => {
370 let mut partial_mmr = self.get_current_partial_mmr().await?;
373 let block_header = self
374 .get_and_store_authenticated_block(
375 inclusion_proof.location().block_num(),
376 &mut partial_mmr,
377 )
378 .await?;
379
380 self.cache_partial_mmr(partial_mmr).await?;
381
382 let tag = metadata.tag();
383 let mut note_record = note_record;
384 let note_changed =
385 note_record.inclusion_proof_received(inclusion_proof, metadata)?;
386
387 if note_record.block_header_received(&block_header)? | note_changed {
388 self.store
389 .remove_note_tag(NoteTagRecord::with_note_source(
390 tag,
391 note_record.details_commitment(),
392 ))
393 .await?;
394
395 note_records.push(Some(note_record));
396 } else {
397 note_records.push(None);
398 }
399 },
400 None => {
401 note_records.push(Some(note_record));
402 },
403 }
404 }
405
406 Ok(note_records)
407 }
408
409 async fn check_expected_notes(
417 &mut self,
418 request_block_num: BlockNumber,
419 expected_notes: Vec<(NoteDetailsCommitment, &NoteTag)>,
421 ) -> Result<BTreeMap<NoteDetailsCommitment, (NoteMetadata, NoteInclusionProof)>, ClientError>
422 {
423 let tracked_tags: BTreeSet<NoteTag> = expected_notes.iter().map(|(_, tag)| **tag).collect();
424 let mut retrieved_proofs = BTreeMap::new();
425 let current_block_num = self.get_sync_height().await?;
426
427 if request_block_num > current_block_num {
428 return Ok(retrieved_proofs);
429 }
430
431 let blocks = self
432 .rpc_api
433 .sync_notes(request_block_num, current_block_num, &tracked_tags)
434 .await
435 .map_err(ClientError::RpcError)?;
436
437 for block in &blocks {
438 if block.block_header.block_num() > current_block_num {
439 break;
440 }
441
442 for sync_note in block.notes.values() {
443 let Some((commitment, _)) = expected_notes.iter().find(|(commitment, _)| {
444 NoteId::new(*commitment, sync_note.metadata()) == *sync_note.note_id()
445 }) else {
446 continue;
447 };
448
449 retrieved_proofs.insert(
450 *commitment,
451 (*sync_note.metadata(), sync_note.inclusion_proof().clone()),
452 );
453 }
454 }
455
456 Ok(retrieved_proofs)
457 }
458}
459
460fn ensure_not_processing(previous_note: Option<&InputNoteRecord>) -> Result<(), ClientError> {
466 if let Some(note) = previous_note
467 && note.is_processing()
468 {
469 return Err(ClientError::NoteImportError(format!(
470 "Can't overwrite note with details commitment {} as it's currently being processed",
471 note.details_commitment().to_hex(),
472 )));
473 }
474 Ok(())
475}