Skip to main content

miden_client/note/
note_update_tracker.rs

1use alloc::collections::BTreeMap;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::block::{BlockHeader, BlockNumber};
5use miden_protocol::note::{
6    Note,
7    NoteAttachments,
8    NoteDetailsCommitment,
9    NoteHeader,
10    NoteId,
11    NoteInclusionProof,
12    NoteMetadata,
13    Nullifier,
14};
15use miden_standards::note::NetworkAccountTarget;
16use miden_tx::utils::serde::{
17    ByteReader,
18    ByteWriter,
19    Deserializable,
20    DeserializationError,
21    Serializable,
22};
23
24use crate::ClientError;
25use crate::rpc::domain::note::CommittedNote;
26use crate::store::{InputNoteRecord, OutputNoteRecord};
27use crate::transaction::{TransactionRecord, TransactionStatus};
28
29// NOTE CONSUMPTION
30// ================================================================================================
31
32/// A note consumption event observed on chain.
33pub struct NoteConsumption {
34    /// The nullifier of the consumed note.
35    pub nullifier: Nullifier,
36    /// The block number at which the note consumption was registered on chain.
37    pub block_num: BlockNumber,
38    /// The account ID of the consumer of the note. Will be set if the note was consumed by a
39    /// transaction submitted outside this client by an account that is tracked locally.
40    /// Otherwise, it will be `None`.
41    pub external_consumer: Option<AccountId>,
42}
43
44// NOTE UPDATE
45// ================================================================================================
46
47/// Represents the possible types of updates that can be applied to a note in a
48/// [`NoteUpdateTracker`].
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50#[repr(u8)]
51pub enum NoteUpdateType {
52    /// Indicates that the note was already tracked but it was not updated.
53    None = 0,
54    /// Indicates that the note is new and should be inserted in the store.
55    Insert = 1,
56    /// Indicates that the note was already tracked and should be updated.
57    Update = 2,
58    /// Indicates that a previously-tracked metadata-less (`Expected`) note has just been committed.
59    /// It must be persisted as a full-row insert (like [`Self::Insert`]) so its now-known `note_id`
60    /// and `nullifier` columns are written, but for reporting it is a *committed* tracked note —
61    /// not a newly-discovered one — so it is summarized under committed notes, not new notes.
62    InsertCommitted = 3,
63}
64
65impl TryFrom<u8> for NoteUpdateType {
66    type Error = u8;
67
68    fn try_from(value: u8) -> Result<Self, Self::Error> {
69        match value {
70            0 => Ok(NoteUpdateType::None),
71            1 => Ok(NoteUpdateType::Insert),
72            2 => Ok(NoteUpdateType::Update),
73            3 => Ok(NoteUpdateType::InsertCommitted),
74            other => Err(other),
75        }
76    }
77}
78
79/// Represents the possible states of an input note record in a [`NoteUpdateTracker`].
80#[derive(Clone, Debug, PartialEq)]
81pub struct InputNoteUpdate {
82    /// Input note being updated.
83    note: InputNoteRecord,
84    /// Type of the note update.
85    update_type: NoteUpdateType,
86}
87
88impl InputNoteUpdate {
89    /// Creates a new [`InputNoteUpdate`] with the provided note with a `None` update type.
90    fn new_none(note: InputNoteRecord) -> Self {
91        Self { note, update_type: NoteUpdateType::None }
92    }
93
94    /// Creates a new [`InputNoteUpdate`] with the provided note with an `Insert` update type.
95    fn new_insert(note: InputNoteRecord) -> Self {
96        Self {
97            note,
98            update_type: NoteUpdateType::Insert,
99        }
100    }
101
102    /// Creates a new [`InputNoteUpdate`] with the provided note with an `Update` update type.
103    fn new_update(note: InputNoteRecord) -> Self {
104        Self {
105            note,
106            update_type: NoteUpdateType::Update,
107        }
108    }
109
110    /// Creates a new [`InputNoteUpdate`] for a previously-tracked expected note that has just been
111    /// committed (see [`NoteUpdateType::InsertCommitted`]).
112    fn new_insert_committed(note: InputNoteRecord) -> Self {
113        Self {
114            note,
115            update_type: NoteUpdateType::InsertCommitted,
116        }
117    }
118
119    /// Returns a reference the inner note record.
120    pub fn inner(&self) -> &InputNoteRecord {
121        &self.note
122    }
123
124    /// Returns a mutable reference to the inner note record. If the update type is `None` or
125    /// `Update`, it will be set to `Update`; insert-typed updates keep their type.
126    fn inner_mut(&mut self) -> &mut InputNoteRecord {
127        self.update_type = match self.update_type {
128            NoteUpdateType::None | NoteUpdateType::Update => NoteUpdateType::Update,
129            NoteUpdateType::Insert => NoteUpdateType::Insert,
130            NoteUpdateType::InsertCommitted => NoteUpdateType::InsertCommitted,
131        };
132
133        &mut self.note
134    }
135
136    /// Returns the type of the note update.
137    pub fn update_type(&self) -> &NoteUpdateType {
138        &self.update_type
139    }
140
141    /// Returns the identifier of the inner note. Returns `None` when the underlying
142    /// [`InputNoteRecord`] has no metadata (see [`InputNoteRecord::id`]).
143    pub fn id(&self) -> Option<NoteId> {
144        self.note.id()
145    }
146
147    /// Returns the per-account position of the consuming transaction within the account's
148    /// execution chain for the block. `None` for non-consumed notes or when the order has not
149    /// been determined yet.
150    pub fn consumed_tx_order(&self) -> Option<u32> {
151        self.note.state().consumed_tx_order()
152    }
153}
154
155/// Represents the possible states of an output note record in a [`NoteUpdateTracker`].
156#[derive(Clone, Debug, PartialEq)]
157pub struct OutputNoteUpdate {
158    /// Output note being updated.
159    note: OutputNoteRecord,
160    /// Type of the note update.
161    update_type: NoteUpdateType,
162}
163
164impl OutputNoteUpdate {
165    /// Creates a new [`OutputNoteUpdate`] with the provided note with a `None` update type.
166    fn new_none(note: OutputNoteRecord) -> Self {
167        Self { note, update_type: NoteUpdateType::None }
168    }
169
170    /// Creates a new [`OutputNoteUpdate`] with the provided note with an `Insert` update type.
171    fn new_insert(note: OutputNoteRecord) -> Self {
172        Self {
173            note,
174            update_type: NoteUpdateType::Insert,
175        }
176    }
177
178    /// Creates a new [`OutputNoteUpdate`] with the provided note with an `Update` update type.
179    fn new_update(note: OutputNoteRecord) -> Self {
180        Self {
181            note,
182            update_type: NoteUpdateType::Update,
183        }
184    }
185
186    /// Returns a reference the inner note record.
187    pub fn inner(&self) -> &OutputNoteRecord {
188        &self.note
189    }
190
191    /// Returns a mutable reference to the inner note record. If the update type is `None` or
192    /// `Update`, it will be set to `Update`.
193    fn inner_mut(&mut self) -> &mut OutputNoteRecord {
194        self.update_type = match self.update_type {
195            NoteUpdateType::None | NoteUpdateType::Update => NoteUpdateType::Update,
196            // Output notes are never assigned `InsertCommitted` (it is input-note specific), but
197            // the match must be exhaustive; treat it as an insert.
198            NoteUpdateType::Insert | NoteUpdateType::InsertCommitted => NoteUpdateType::Insert,
199        };
200
201        &mut self.note
202    }
203
204    /// Returns the type of the note update.
205    pub fn update_type(&self) -> &NoteUpdateType {
206        &self.update_type
207    }
208
209    /// Returns the identifier of the inner note.
210    pub fn id(&self) -> NoteId {
211        self.note.id()
212    }
213}
214
215// NOTE UPDATE TRACKER
216// ================================================================================================
217
218/// Contains note changes to apply to the store.
219///
220/// This includes new notes that have been created and existing notes that have been updated. The
221/// tracker also lets state changes be applied to the contained notes, this allows for already
222/// updated notes to be further updated as new information is received.
223#[derive(Clone, Debug, Default, PartialEq)]
224pub struct NoteUpdateTracker {
225    /// A map of new and updated input note records to be upserted in the store.
226    // TODO: consider keying all input notes by `NoteDetailsCommitment` (always available) instead
227    // of this `NoteId`/commitment split, which would remove `expected_input_notes` and the
228    // `InsertCommitted` re-keying. `NoteId = hash(details_commitment, metadata)` requires
229    // metadata, so it is absent until a note commits.
230    input_notes: BTreeMap<NoteId, InputNoteUpdate>,
231    /// Metadata-less notes keyed by details commitment (they have no `NoteId` yet); moved to
232    /// `input_notes` once a committed note supplies their metadata.
233    expected_input_notes: BTreeMap<NoteDetailsCommitment, InputNoteUpdate>,
234    /// A map of updated output note records to be upserted in the store.
235    output_notes: BTreeMap<NoteId, OutputNoteUpdate>,
236    /// Fast lookup map from nullifier to input note id.
237    input_notes_by_nullifier: BTreeMap<Nullifier, NoteId>,
238    /// Fast lookup map from nullifier to output note id.
239    output_notes_by_nullifier: BTreeMap<Nullifier, NoteId>,
240    /// Map from nullifier to its per-account position in the consuming transaction order.
241    /// Nullifiers from the same account are in execution order; ordering across different
242    /// accounts is not guaranteed.
243    nullifier_order: BTreeMap<Nullifier, u32>,
244}
245
246impl NoteUpdateTracker {
247    /// Creates a [`NoteUpdateTracker`] with already-tracked notes.
248    pub fn new(
249        input_notes: impl IntoIterator<Item = InputNoteRecord>,
250        output_notes: impl IntoIterator<Item = OutputNoteRecord>,
251    ) -> Self {
252        let mut tracker = Self::default();
253        for note in input_notes {
254            tracker.insert_input_note(note, NoteUpdateType::None);
255        }
256        for note in output_notes {
257            tracker.insert_output_note(note, NoteUpdateType::None);
258        }
259
260        tracker
261    }
262
263    /// Creates a [`NoteUpdateTracker`] for updates related to transactions.
264    ///
265    /// A transaction can:
266    ///
267    /// - Create input notes
268    /// - Update existing input notes (by consuming them)
269    /// - Create output notes
270    pub fn for_transaction_updates(
271        new_input_notes: impl IntoIterator<Item = InputNoteRecord>,
272        updated_input_notes: impl IntoIterator<Item = InputNoteRecord>,
273        new_output_notes: impl IntoIterator<Item = OutputNoteRecord>,
274    ) -> Self {
275        let mut tracker = Self::default();
276
277        for note in new_input_notes {
278            tracker.insert_input_note(note, NoteUpdateType::Insert);
279        }
280
281        for note in updated_input_notes {
282            tracker.insert_input_note(note, NoteUpdateType::Update);
283        }
284
285        for note in new_output_notes {
286            tracker.insert_output_note(note, NoteUpdateType::Insert);
287        }
288
289        tracker
290    }
291
292    // GETTERS
293    // --------------------------------------------------------------------------------------------
294
295    /// Returns all input note records that have been updated.
296    ///
297    /// This may include:
298    /// - New notes that have been created that should be inserted.
299    /// - Existing tracked notes that should be updated.
300    ///
301    /// Metadata-less expected notes (e.g. future notes created by a transaction, such as swap
302    /// payback notes) are included as well: they have no `NoteId` yet but must still be persisted
303    /// and have their tags registered. The `update_type` filter ensures notes merely loaded as
304    /// already-tracked context (`NoteUpdateType::None`) are not re-emitted.
305    pub fn updated_input_notes(&self) -> impl Iterator<Item = &InputNoteUpdate> {
306        self.input_notes
307            .values()
308            .chain(self.expected_input_notes.values())
309            .filter(|note| {
310                matches!(
311                    note.update_type,
312                    NoteUpdateType::Insert
313                        | NoteUpdateType::Update
314                        | NoteUpdateType::InsertCommitted
315                )
316            })
317    }
318
319    /// Returns the ids of updated input notes that are now consumed, by tracking key. Consumed
320    /// states carry no metadata, so `InputNoteRecord::id` is `None`; the key (the id assigned at
321    /// commit) is used instead.
322    pub fn consumed_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
323        self.input_notes
324            .iter()
325            .filter(|(_, update)| {
326                matches!(
327                    update.update_type,
328                    NoteUpdateType::Insert
329                        | NoteUpdateType::Update
330                        | NoteUpdateType::InsertCommitted
331                ) && update.inner().is_consumed()
332            })
333            .map(|(note_id, _)| *note_id)
334    }
335
336    /// Returns all output note records that have been updated.
337    ///
338    /// This may include:
339    /// - New notes that have been created that should be inserted.
340    /// - Existing tracked notes that should be updated.
341    pub fn updated_output_notes(&self) -> impl Iterator<Item = &OutputNoteUpdate> {
342        self.output_notes.values().filter(|note| {
343            matches!(note.update_type, NoteUpdateType::Insert | NoteUpdateType::Update)
344        })
345    }
346
347    /// Returns whether no new note-related information has been retrieved.
348    pub fn is_empty(&self) -> bool {
349        self.input_notes.is_empty()
350            && self.output_notes.is_empty()
351            && self.expected_input_notes.is_empty()
352    }
353
354    /// Returns input and output note unspent nullifiers.
355    pub fn unspent_nullifiers(&self) -> impl Iterator<Item = Nullifier> {
356        let input_note_unspent_nullifiers = self
357            .input_notes
358            .values()
359            .filter(|note| !note.inner().is_consumed())
360            .filter_map(|note| note.inner().nullifier());
361
362        let output_note_unspent_nullifiers = self
363            .output_notes
364            .values()
365            .filter(|note| !note.inner().is_consumed())
366            .filter_map(|note| note.inner().nullifier());
367
368        input_note_unspent_nullifiers.chain(output_note_unspent_nullifiers)
369    }
370
371    /// Appends nullifiers to the per-account ordered nullifier list.
372    ///
373    /// Nullifiers from the same account must be in execution order; ordering across different
374    /// accounts is not guaranteed.
375    pub fn extend_nullifiers(&mut self, nullifiers: impl IntoIterator<Item = Nullifier>) {
376        for nullifier in nullifiers {
377            let next_pos =
378                u32::try_from(self.nullifier_order.len()).expect("nullifier count exceeds u32");
379            self.nullifier_order.entry(nullifier).or_insert(next_pos);
380        }
381    }
382
383    // UPDATE METHODS
384    // --------------------------------------------------------------------------------------------
385
386    /// Inserts the new public note data into the tracker. This method doesn't check the relevance
387    /// of the note, so it should only be used for notes that are guaranteed to be relevant to the
388    /// client.
389    pub(crate) fn apply_new_public_note(
390        &mut self,
391        mut public_note_data: InputNoteRecord,
392        block_header: &BlockHeader,
393    ) -> Result<(), ClientError> {
394        public_note_data.block_header_received(block_header)?;
395        self.insert_input_note(public_note_data, NoteUpdateType::Insert);
396
397        Ok(())
398    }
399
400    /// Applies the necessary state transitions to the [`NoteUpdateTracker`] when a note is
401    /// committed in a block and returns whether the committed note is tracked as input note.
402    pub(crate) fn apply_committed_note_state_transitions(
403        &mut self,
404        committed_note: &CommittedNote,
405        block_header: &BlockHeader,
406        attachments: Option<&NoteAttachments>,
407    ) -> Result<bool, ClientError> {
408        let inclusion_proof = committed_note.inclusion_proof().clone();
409        let metadata = *committed_note.metadata();
410        let note_id = *committed_note.note_id();
411
412        let is_tracked_as_input_note =
413            if let Some(input_note_record) = self.get_input_note_by_id(note_id) {
414                input_note_record.inclusion_proof_received(inclusion_proof.clone(), metadata)?;
415                input_note_record.block_header_received(block_header)?;
416                if let Some(attachments) = attachments {
417                    input_note_record.set_attachments(attachments.clone());
418                }
419
420                true
421            } else if let Some(commitment) = self.expected_note_matching(note_id, &metadata) {
422                // A metadata-less note whose id, with the committed metadata, equals this note id:
423                // evolve it into a full record.
424                let mut update = self
425                    .expected_input_notes
426                    .remove(&commitment)
427                    .expect("commitment was just matched against the expected notes");
428                let record = &mut update.note;
429                record.inclusion_proof_received(inclusion_proof.clone(), metadata)?;
430                record.block_header_received(block_header)?;
431                if let Some(attachments) = attachments {
432                    record.set_attachments(attachments.clone());
433                }
434
435                // `InsertCommitted` so the now-known `note_id`/`nullifier` columns are persisted
436                // (a full-row insert), while still being reported as a committed tracked note
437                // rather than a newly-discovered one. Re-key by the now-available id.
438                let nullifier = record.nullifier().expect("note with an id has metadata");
439                self.input_notes_by_nullifier.insert(nullifier, note_id);
440                self.input_notes
441                    .insert(note_id, InputNoteUpdate::new_insert_committed(update.note));
442
443                true
444            } else {
445                false
446            };
447
448        self.try_commit_output_note(note_id, inclusion_proof)?;
449
450        Ok(is_tracked_as_input_note)
451    }
452
453    /// Applies inclusion proofs from the transaction sync response to tracked output notes.
454    ///
455    /// This transitions output notes from `Expected` to `Committed` state using the
456    /// inclusion proofs returned by `SyncTransactions`.
457    pub(crate) fn apply_output_note_inclusion_proofs(
458        &mut self,
459        committed_notes: &[CommittedNote],
460    ) -> Result<(), ClientError> {
461        for committed_note in committed_notes {
462            self.try_commit_output_note(
463                *committed_note.note_id(),
464                committed_note.inclusion_proof().clone(),
465            )?;
466        }
467        Ok(())
468    }
469
470    /// Marks an erased note as consumed.
471    ///
472    /// This handles notes that were erased due to same-batch note erasure: the note was
473    /// created and consumed within the same batch, so it never appeared in the block body.
474    /// The `block_num` is the block in which the creating transaction was committed.
475    ///
476    /// The consumer account id is derived from the tracked input record's attachments (a
477    /// [`NetworkAccountTarget`], when present), not from the erased-note RPC stream, which delivers
478    /// only a [`NoteHeader`]. When no such attachment is present the consumer is left unknown.
479    pub(crate) fn mark_erased_note_as_consumed(
480        &mut self,
481        note_header: &NoteHeader,
482        block_num: BlockNumber,
483    ) -> Result<(), ClientError> {
484        let note_id = note_header.id();
485
486        if let Some(output_note) = self.get_output_note_by_id(note_id)
487            && !output_note.is_consumed()
488            && !output_note.is_committed()
489            && let Some(nullifier) = output_note.nullifier()
490        {
491            output_note.nullifier_received(nullifier, block_num)?;
492        }
493
494        if let Some(input_note_update) = self.input_notes.get_mut(&note_id)
495            && !input_note_update.inner().is_consumed()
496            && let Some(nullifier) = input_note_update.inner().nullifier()
497        {
498            let consumer_account =
499                NetworkAccountTarget::try_from(input_note_update.inner().attachments())
500                    .ok()
501                    .map(|target| target.target_id());
502            input_note_update.inner_mut().consumed_externally(
503                nullifier,
504                block_num,
505                consumer_account,
506            )?;
507            input_note_update.inner_mut().set_consumed_tx_order(Some(0));
508        }
509
510        Ok(())
511    }
512
513    /// Builds a consumed input note record from a tracked output note and inserts it.
514    ///
515    /// Used when an output note is consumed externally and the client should also surface
516    /// it as a consumed input — for example, when the same client tracks both the sender
517    /// and the consumer of the note. No-op if the input is already tracked, the output is
518    /// not tracked, or the output cannot be converted to a [`Note`].
519    fn try_insert_consumed_input_from_output(
520        &mut self,
521        note_id: NoteId,
522        consumer: AccountId,
523        block_num: BlockNumber,
524        consumed_tx_order: Option<u32>,
525    ) -> Result<(), ClientError> {
526        if self.input_notes.contains_key(&note_id) {
527            return Ok(());
528        }
529        let Some(output_note) = self.output_notes.get(&note_id) else {
530            return Ok(());
531        };
532        let Ok(note) = Note::try_from(output_note.inner().clone()) else {
533            return Ok(());
534        };
535
536        let mut input_record = InputNoteRecord::from(note);
537        let nullifier =
538            input_record.nullifier().expect("record built from a full note has metadata");
539        input_record.consumed_externally(nullifier, block_num, Some(consumer))?;
540        input_record.set_consumed_tx_order(consumed_tx_order);
541        self.insert_input_note(input_record, NoteUpdateType::Insert);
542        Ok(())
543    }
544
545    /// If the note is tracked as an output note, transitions it to `Committed` with the
546    /// given inclusion proof. No-op if the note is not tracked.
547    fn try_commit_output_note(
548        &mut self,
549        note_id: NoteId,
550        inclusion_proof: NoteInclusionProof,
551    ) -> Result<(), ClientError> {
552        if let Some(output_note) = self.get_output_note_by_id(note_id) {
553            output_note.inclusion_proof_received(inclusion_proof)?;
554        }
555        Ok(())
556    }
557
558    /// Applies the necessary state transitions to the [`NoteUpdateTracker`] when a note is
559    /// nullified in a block.
560    ///
561    /// For input note records two possible scenarios are considered:
562    /// 1. The note was being processed by a local transaction that just got committed.
563    /// 2. The note was consumed by a transaction not submitted by this client. This includes
564    ///    consumption by untracked accounts as well as consumption by tracked accounts whose
565    ///    transactions were submitted by other client instances. If a local transaction was
566    ///    processing the note and it didn't get committed, the transaction should be discarded.
567    ///
568    /// If the note is tracked as an output but not as an input (e.g. the client tracks both the
569    /// sender and the consumer), a new input record is created from the output details so the
570    /// consumption surfaces through `InputNoteReader`.
571    pub(crate) fn apply_note_consumption<'a>(
572        &mut self,
573        consumption: &NoteConsumption,
574        mut committed_transactions: impl Iterator<Item = &'a TransactionRecord>,
575    ) -> Result<(), ClientError> {
576        let nullifier = consumption.nullifier;
577        let block_num = consumption.block_num;
578        let external_consumer = consumption.external_consumer;
579        let order = self.get_nullifier_order(nullifier);
580        let input_present = self.input_notes_by_nullifier.contains_key(&nullifier);
581
582        if let Some(input_note_update) = self.get_input_note_update_by_nullifier(nullifier) {
583            if let Some(consumer_transaction) = committed_transactions
584                .find(|t| input_note_update.inner().consumer_transaction_id() == Some(&t.id))
585            {
586                // The note was being processed by a local transaction that just got committed
587                if let TransactionStatus::Committed { block_number, .. } =
588                    consumer_transaction.status
589                {
590                    input_note_update
591                        .inner_mut()
592                        .transaction_committed(consumer_transaction.id, block_number)?;
593                }
594            } else {
595                // The note was consumed by a transaction not submitted by this client.
596                // If the consuming account is tracked, external_consumer will be Some.
597                input_note_update.inner_mut().consumed_externally(
598                    nullifier,
599                    block_num,
600                    external_consumer,
601                )?;
602            }
603            input_note_update.inner_mut().set_consumed_tx_order(order);
604        }
605
606        if let Some(output_note_record) = self.get_output_note_by_nullifier(nullifier) {
607            output_note_record.nullifier_received(nullifier, block_num)?;
608        }
609
610        if !input_present
611            && let Some(consumer) = external_consumer
612            && let Some(note_id) = self.output_notes_by_nullifier.get(&nullifier).copied()
613        {
614            self.try_insert_consumed_input_from_output(note_id, consumer, block_num, order)?;
615        }
616
617        Ok(())
618    }
619
620    // PRIVATE HELPERS
621    // --------------------------------------------------------------------------------------------
622
623    /// Returns the position of the given nullifier in the consuming transaction order, or `None`
624    /// if it is not present.
625    fn get_nullifier_order(&self, nullifier: Nullifier) -> Option<u32> {
626        self.nullifier_order.get(&nullifier).copied()
627    }
628
629    /// Returns a mutable reference to the input note record with the provided ID if it exists.
630    fn get_input_note_by_id(&mut self, note_id: NoteId) -> Option<&mut InputNoteRecord> {
631        self.input_notes.get_mut(&note_id).map(InputNoteUpdate::inner_mut)
632    }
633
634    /// Returns the details commitment of a tracked metadata-less note whose id, combined with
635    /// `metadata`, equals `note_id` — i.e. the committed note is that imported note.
636    fn expected_note_matching(
637        &self,
638        note_id: NoteId,
639        metadata: &NoteMetadata,
640    ) -> Option<NoteDetailsCommitment> {
641        self.expected_input_notes
642            .keys()
643            .copied()
644            .find(|commitment| NoteId::new(*commitment, metadata) == note_id)
645    }
646
647    /// Returns a mutable reference to the output note record with the provided ID if it exists.
648    fn get_output_note_by_id(&mut self, note_id: NoteId) -> Option<&mut OutputNoteRecord> {
649        self.output_notes.get_mut(&note_id).map(OutputNoteUpdate::inner_mut)
650    }
651
652    /// Returns a mutable reference to the input note update with the provided nullifier if it
653    /// exists.
654    fn get_input_note_update_by_nullifier(
655        &mut self,
656        nullifier: Nullifier,
657    ) -> Option<&mut InputNoteUpdate> {
658        let note_id = self.input_notes_by_nullifier.get(&nullifier).copied()?;
659        self.input_notes.get_mut(&note_id)
660    }
661
662    /// Returns a mutable reference to the output note record with the provided nullifier if it
663    /// exists.
664    fn get_output_note_by_nullifier(
665        &mut self,
666        nullifier: Nullifier,
667    ) -> Option<&mut OutputNoteRecord> {
668        let note_id = self.output_notes_by_nullifier.get(&nullifier).copied()?;
669        self.output_notes.get_mut(&note_id).map(OutputNoteUpdate::inner_mut)
670    }
671
672    /// Insert an input note update
673    fn insert_input_note(&mut self, note: InputNoteRecord, update_type: NoteUpdateType) {
674        let update = match update_type {
675            NoteUpdateType::None => InputNoteUpdate::new_none(note),
676            NoteUpdateType::Insert => InputNoteUpdate::new_insert(note),
677            NoteUpdateType::Update => InputNoteUpdate::new_update(note),
678            NoteUpdateType::InsertCommitted => InputNoteUpdate::new_insert_committed(note),
679        };
680
681        if let Some(note_id) = update.inner().id() {
682            // A note with metadata supersedes any metadata-less record for the same note.
683            self.expected_input_notes.remove(&update.inner().details_commitment());
684            let nullifier = update.inner().nullifier().expect("note with an id has metadata");
685            self.input_notes_by_nullifier.insert(nullifier, note_id);
686            self.input_notes.insert(note_id, update);
687        } else {
688            // No metadata yet means no `NoteId` and no computable nullifier; track by details
689            // commitment until a committed note supplies the metadata to evolve it.
690            let commitment = update.inner().details_commitment();
691            self.expected_input_notes.insert(commitment, update);
692        }
693    }
694
695    /// Insert an output note update
696    fn insert_output_note(&mut self, note: OutputNoteRecord, update_type: NoteUpdateType) {
697        let note_id = note.id();
698        if let Some(nullifier) = note.nullifier() {
699            self.output_notes_by_nullifier.insert(nullifier, note_id);
700        }
701        let update = match update_type {
702            NoteUpdateType::None => OutputNoteUpdate::new_none(note),
703            NoteUpdateType::Update => OutputNoteUpdate::new_update(note),
704            // Output notes are never assigned `InsertCommitted`; treat it as an insert for
705            // exhaustiveness.
706            NoteUpdateType::Insert | NoteUpdateType::InsertCommitted => {
707                OutputNoteUpdate::new_insert(note)
708            },
709        };
710        self.output_notes.insert(note_id, update);
711    }
712}
713
714// SERIALIZATION
715// ================================================================================================
716
717impl Serializable for NoteUpdateType {
718    fn write_into<W: ByteWriter>(&self, target: &mut W) {
719        target.write_u8(*self as u8);
720    }
721}
722
723impl Deserializable for NoteUpdateType {
724    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
725        NoteUpdateType::try_from(source.read_u8()?).map_err(|val| {
726            DeserializationError::InvalidValue(format!("invalid note update type: {val}"))
727        })
728    }
729}
730
731impl Serializable for InputNoteUpdate {
732    fn write_into<W: ByteWriter>(&self, target: &mut W) {
733        self.note.write_into(target);
734        self.update_type.write_into(target);
735    }
736}
737
738impl Deserializable for InputNoteUpdate {
739    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
740        let note = InputNoteRecord::read_from(source)?;
741        let update_type = NoteUpdateType::read_from(source)?;
742        Ok(Self { note, update_type })
743    }
744}
745
746impl Serializable for OutputNoteUpdate {
747    fn write_into<W: ByteWriter>(&self, target: &mut W) {
748        self.note.write_into(target);
749        self.update_type.write_into(target);
750    }
751}
752
753impl Deserializable for OutputNoteUpdate {
754    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
755        let note = OutputNoteRecord::read_from(source)?;
756        let update_type = NoteUpdateType::read_from(source)?;
757        Ok(Self { note, update_type })
758    }
759}
760
761impl Serializable for NoteUpdateTracker {
762    fn write_into<W: ByteWriter>(&self, target: &mut W) {
763        // `input_notes_by_nullifier` and `output_notes_by_nullifier` are lookup indices that can
764        // be reconstructed from `input_notes` and `output_notes`, so they are not serialized.
765        self.input_notes.write_into(target);
766        self.output_notes.write_into(target);
767        self.expected_input_notes.write_into(target);
768        self.nullifier_order.write_into(target);
769    }
770}
771
772impl Deserializable for NoteUpdateTracker {
773    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
774        let input_notes = BTreeMap::<NoteId, InputNoteUpdate>::read_from(source)?;
775        let output_notes = BTreeMap::<NoteId, OutputNoteUpdate>::read_from(source)?;
776        let expected_input_notes =
777            BTreeMap::<NoteDetailsCommitment, InputNoteUpdate>::read_from(source)?;
778        let nullifier_order = BTreeMap::<Nullifier, u32>::read_from(source)?;
779
780        let input_notes_by_nullifier = input_notes
781            .iter()
782            .map(|(note_id, update)| {
783                (update.inner().nullifier().expect("note with an id has metadata"), *note_id)
784            })
785            .collect();
786        let output_notes_by_nullifier = output_notes
787            .iter()
788            .filter_map(|(note_id, update)| {
789                update.inner().nullifier().map(|nullifier| (nullifier, *note_id))
790            })
791            .collect();
792
793        Ok(Self {
794            input_notes,
795            expected_input_notes,
796            output_notes,
797            input_notes_by_nullifier,
798            output_notes_by_nullifier,
799            nullifier_order,
800        })
801    }
802}