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