miden_client/note/
import.rs

1//! Provides note importing methods.
2//!
3//! This module allows users to import notes into the client's store.
4//! Depending on the variant of [`NoteFile`] provided, the client will either fetch note details
5//! from the network or create a new note record from supplied data. If a note already exists in
6//! the store, it is updated with the new information. Additionally, the appropriate note tag
7//! is tracked based on the imported note's metadata.
8//!
9//! For more specific information on how the process is performed, refer to the docs for
10//! [`Client::import_note()`].
11use 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
25/// Note importing methods.
26impl Client {
27    // INPUT NOTE CREATION
28    // --------------------------------------------------------------------------------------------
29
30    /// Imports a new input note into the client's store. The information stored depends on the
31    /// type of note file provided. If the note existed previously, it will be updated with the
32    /// new information. The tag specified by the `NoteFile` will start being tracked.
33    ///
34    /// - If the note file is a [`NoteFile::NoteId`], the note is fetched from the node and stored
35    ///   in the client's store. If the note is private or doesn't exist, an error is returned.
36    /// - If the note file is a [`NoteFile::NoteDetails`], a new note is created with the provided
37    ///   details and tag.
38    /// - If the note file is a [`NoteFile::NoteWithProof`], the note is stored with the provided
39    ///   inclusion proof and metadata. The block header data is only fetched from the node if the
40    ///   note is committed in the past relative to the client.
41    ///
42    /// # Errors
43    ///
44    /// - If an attempt is made to overwrite a note that is currently processing.
45    pub async fn import_note(&mut self, note_file: NoteFile) -> Result<NoteId, ClientError> {
46        let id = match &note_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 the note is already in the store and is in the state processing we return an error.
55        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    // HELPERS
86    // ================================================================================================
87
88    /// Builds a note record from the note ID. If a note with the same ID was already stored it is
89    /// passed via `previous_note` so it can be updated. The note information is fetched from the
90    /// node and stored in the client's store.
91    ///
92    /// # Errors:
93    /// - If the note doesn't exist on the node.
94    /// - If the note exists but is private.
95    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    /// Builds a note record from the note and inclusion proof. If a note with the same ID was
131    /// already stored it is passed via `previous_note` so it can be updated. The note's
132    /// nullifier is used to determine if the note has been consumed in the node and gives it
133    /// the correct state.
134    ///
135    /// If the note isn't consumed and it was committed in the past relative to the client, then
136    /// the MMR for the relevant block is fetched from the node and stored.
137    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                &note_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((&note_record).try_into()?).await?;
187
188                Ok(Some(note_record))
189            } else {
190                Ok(None)
191            }
192        }
193    }
194
195    /// Builds a note record from the note details. If a note with the same ID was already stored it
196    /// is passed via `previous_note` so it can be updated.
197    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    /// Checks if a note with the given `note_tag` and ID is present in the chain between the
249    /// `request_block_num` and the current block. If found it returns its metadata and inclusion
250    /// proof.
251    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            // This means that notes with that note_tag were found.
270            // Therefore, we should check if a note with the same id was found.
271            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                // This means that a note with the same id was found.
276                // Therefore, we should mark the note as committed.
277                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            // This means that a note with the same id was not found.
292            // Therefore, we should request again for sync_notes with the same note_tag
293            // and with the block_num of the last block header
294            // (sync_notes.block_header.unwrap()).
295            request_block_num = sync_notes.block_header.block_num();
296        }
297    }
298}