1use alloc::string::ToString;
12
13use miden_objects::{
14 block::BlockNumber,
15 note::{Note, NoteDetails, NoteFile, NoteId, NoteInclusionProof, NoteMetadata, NoteTag},
16};
17
18use crate::{
19 Client, ClientError,
20 rpc::{RpcError, domain::note::NetworkNote},
21 store::{InputNoteRecord, InputNoteState, input_note_states::ExpectedNoteState},
22 sync::NoteTagRecord,
23};
24
25impl Client {
27 pub async fn import_note(&mut self, note_file: NoteFile) -> Result<NoteId, ClientError> {
46 let id = match ¬e_file {
47 NoteFile::NoteId(id) => *id,
48 NoteFile::NoteDetails { details, .. } => details.id(),
49 NoteFile::NoteWithProof(note, _) => note.id(),
50 };
51
52 let previous_note = self.get_input_note(id).await?;
53
54 if let Some(true) = previous_note.as_ref().map(InputNoteRecord::is_processing) {
56 return Err(ClientError::NoteImportError(format!(
57 "Can't overwrite note with id {id} as it's currently being processed",
58 )));
59 }
60
61 let note = match note_file {
62 NoteFile::NoteId(id) => self.import_note_record_by_id(previous_note, id).await?,
63 NoteFile::NoteDetails { details, after_block_num, tag } => {
64 self.import_note_record_by_details(previous_note, details, after_block_num, tag)
65 .await?
66 },
67 NoteFile::NoteWithProof(note, inclusion_proof) => {
68 self.import_note_record_by_proof(previous_note, note, inclusion_proof).await?
69 },
70 };
71
72 if let Some(note) = note {
73 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) = note.state()
74 {
75 self.store
76 .add_note_tag(NoteTagRecord::with_note_source(*tag, note.id()))
77 .await?;
78 }
79 self.store.upsert_input_notes(&[note]).await?;
80 }
81
82 Ok(id)
83 }
84
85 async fn import_note_record_by_id(
96 &self,
97 previous_note: Option<InputNoteRecord>,
98 id: NoteId,
99 ) -> Result<Option<InputNoteRecord>, ClientError> {
100 let network_note = self.rpc_api.get_note_by_id(id).await.map_err(|err| match err {
101 RpcError::NoteNotFound(note_id) => ClientError::NoteNotFoundOnChain(note_id),
102 err => ClientError::RpcError(err),
103 })?;
104
105 let inclusion_proof = network_note.inclusion_proof().clone();
106
107 if let Some(mut previous_note) = previous_note {
108 if previous_note.inclusion_proof_received(inclusion_proof, *network_note.metadata())? {
109 self.store.remove_note_tag((&previous_note).try_into()?).await?;
110
111 Ok(Some(previous_note))
112 } else {
113 Ok(None)
114 }
115 } else {
116 let network_note = match network_note {
117 NetworkNote::Public(note, _) => note,
118 NetworkNote::Private(..) => {
119 return Err(ClientError::NoteImportError(
120 "Incomplete imported note is private".to_string(),
121 ));
122 },
123 };
124
125 self.import_note_record_by_proof(previous_note, network_note, inclusion_proof)
126 .await
127 }
128 }
129
130 async fn import_note_record_by_proof(
138 &self,
139 previous_note: Option<InputNoteRecord>,
140 note: Note,
141 inclusion_proof: NoteInclusionProof,
142 ) -> Result<Option<InputNoteRecord>, ClientError> {
143 let metadata = *note.metadata();
144 let mut note_record = previous_note.unwrap_or(InputNoteRecord::new(
145 note.into(),
146 self.store.get_current_timestamp(),
147 ExpectedNoteState {
148 metadata: Some(metadata),
149 after_block_num: inclusion_proof.location().block_num(),
150 tag: Some(metadata.tag()),
151 }
152 .into(),
153 ));
154
155 if let Some(block_height) = self
156 .rpc_api
157 .get_nullifier_commit_height(
158 ¬e_record.nullifier(),
159 inclusion_proof.location().block_num(),
160 )
161 .await?
162 {
163 if note_record.consumed_externally(note_record.nullifier(), block_height)? {
164 return Ok(Some(note_record));
165 }
166
167 Ok(None)
168 } else {
169 let block_height = inclusion_proof.location().block_num();
170 let current_block_num = self.get_sync_height().await?;
171
172 let mut note_changed =
173 note_record.inclusion_proof_received(inclusion_proof, metadata)?;
174
175 if block_height < current_block_num {
176 let mut current_partial_mmr = self.build_current_partial_mmr(true).await?;
177
178 let block_header = self
179 .get_and_store_authenticated_block(block_height, &mut current_partial_mmr)
180 .await?;
181
182 note_changed |= note_record.block_header_received(&block_header)?;
183 }
184
185 if note_changed {
186 self.store.remove_note_tag((¬e_record).try_into()?).await?;
187
188 Ok(Some(note_record))
189 } else {
190 Ok(None)
191 }
192 }
193 }
194
195 async fn import_note_record_by_details(
198 &mut self,
199 previous_note: Option<InputNoteRecord>,
200 details: NoteDetails,
201 after_block_num: BlockNumber,
202 tag: Option<NoteTag>,
203 ) -> Result<Option<InputNoteRecord>, ClientError> {
204 let mut note_record = previous_note.unwrap_or({
205 InputNoteRecord::new(
206 details,
207 self.store.get_current_timestamp(),
208 ExpectedNoteState { metadata: None, after_block_num, tag }.into(),
209 )
210 });
211
212 let committed_note_data = if let Some(tag) = tag {
213 self.check_expected_note(after_block_num, tag, note_record.details()).await?
214 } else {
215 None
216 };
217
218 match committed_note_data {
219 Some((metadata, inclusion_proof)) => {
220 let mut current_partial_mmr = self.build_current_partial_mmr(true).await?;
221 let block_header = self
222 .get_and_store_authenticated_block(
223 inclusion_proof.location().block_num(),
224 &mut current_partial_mmr,
225 )
226 .await?;
227
228 let note_changed =
229 note_record.inclusion_proof_received(inclusion_proof, metadata)?;
230
231 if note_record.block_header_received(&block_header)? | note_changed {
232 self.store
233 .remove_note_tag(NoteTagRecord::with_note_source(
234 metadata.tag(),
235 note_record.id(),
236 ))
237 .await?;
238
239 Ok(Some(note_record))
240 } else {
241 Ok(None)
242 }
243 },
244 None => Ok(Some(note_record)),
245 }
246 }
247
248 async fn check_expected_note(
252 &mut self,
253 mut request_block_num: BlockNumber,
254 tag: NoteTag,
255 expected_note: &miden_objects::note::NoteDetails,
256 ) -> Result<Option<(NoteMetadata, NoteInclusionProof)>, ClientError> {
257 let current_block_num = self.get_sync_height().await?;
258 loop {
259 if request_block_num > current_block_num {
260 return Ok(None);
261 }
262
263 let sync_notes = self.rpc_api.sync_notes(request_block_num, &[tag]).await?;
264
265 if sync_notes.block_header.block_num() == sync_notes.chain_tip.into() {
266 return Ok(None);
267 }
268
269 let committed_note =
272 sync_notes.notes.iter().find(|note| note.note_id() == &expected_note.id());
273
274 if let Some(note) = committed_note {
275 let note_block_num = sync_notes.block_header.block_num();
278
279 if note_block_num > current_block_num {
280 return Ok(None);
281 }
282
283 let note_inclusion_proof = NoteInclusionProof::new(
284 note_block_num,
285 note.note_index(),
286 note.merkle_path().clone(),
287 )?;
288
289 return Ok(Some((note.metadata(), note_inclusion_proof)));
290 }
291 request_block_num = sync_notes.block_header.block_num();
296 }
297 }
298}