Skip to main content

miden_client/sync/
tag.rs

1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use miden_protocol::Word;
5use miden_protocol::account::{Account, AccountId};
6use miden_protocol::note::{NoteDetailsCommitment, NoteTag};
7use miden_tx::utils::serde::{
8    ByteReader,
9    ByteWriter,
10    Deserializable,
11    DeserializationError,
12    Serializable,
13};
14use tracing::warn;
15
16use crate::Client;
17use crate::errors::ClientError;
18use crate::store::{InputNoteRecord, NoteRecordError};
19
20/// Tag management methods
21impl<AUTH> Client<AUTH> {
22    /// Returns the list of note tags tracked by the client along with their source.
23    ///
24    /// When syncing the state with the node, these tags will be added to the sync request and
25    /// note-related information will be retrieved for notes that have matching tags.
26    ///  The source of the tag indicates its origin. It helps distinguish between:
27    ///  - Tags added manually by the user.
28    ///  - Tags automatically added by the client to track notes.
29    ///  - Tags added for accounts tracked by the client.
30    ///
31    /// Note: Tags for accounts that are being tracked by the client are managed automatically by
32    /// the client and don't need to be added here. That is, notes for managed accounts will be
33    /// retrieved automatically by the client when syncing.
34    pub async fn get_note_tags(&self) -> Result<Vec<NoteTagRecord>, ClientError> {
35        self.store.get_note_tags().await.map_err(Into::into)
36    }
37
38    /// Adds a note tag for the client to track. This tag's source will be marked as `User`.
39    pub async fn add_note_tag(&mut self, tag: NoteTag) -> Result<(), ClientError> {
40        let added = self
41            .store
42            .add_note_tag(NoteTagRecord { tag, source: NoteTagSource::User })
43            .await?;
44        if !added {
45            warn!("Tag {} is already being tracked", tag);
46        }
47        Ok(())
48    }
49
50    /// Removes a note tag for the client to track. Only tags added by the user can be removed.
51    pub async fn remove_note_tag(&mut self, tag: NoteTag) -> Result<(), ClientError> {
52        if self
53            .store
54            .remove_note_tag(NoteTagRecord { tag, source: NoteTagSource::User })
55            .await?
56            == 0
57        {
58            warn!("Tag {} wasn't being tracked", tag);
59        }
60
61        Ok(())
62    }
63}
64
65/// Represents a note tag of which the Store can keep track and retrieve.
66#[derive(Debug, PartialEq, Eq, Clone, Copy)]
67pub struct NoteTagRecord {
68    pub tag: NoteTag,
69    pub source: NoteTagSource,
70}
71
72/// Represents the source of the tag. This is used to differentiate between tags that are added by
73/// the user and tags that are added automatically by the client to track notes .
74#[derive(Debug, PartialEq, Eq, Clone, Copy)]
75pub enum NoteTagSource {
76    /// Tag for notes directed to a tracked account.
77    Account(AccountId),
78    /// Tag for tracked expected notes, identified by the note's details commitment.
79    Note(NoteDetailsCommitment),
80    /// Tag manually added by the user.
81    User,
82    /// Tag for a long-lived subscription, anchored to an opaque 4-felt key that identifies its
83    /// origin (e.g. the id of the note that registered it). Distinct subscriptions may share the
84    /// same [`NoteTag`]; the key keeps them as separate rows so each is tracked and removed
85    /// independently.
86    Subscription(Word),
87}
88
89impl NoteTagRecord {
90    pub fn with_note_source(tag: NoteTag, details_commitment: NoteDetailsCommitment) -> Self {
91        Self {
92            tag,
93            source: NoteTagSource::Note(details_commitment),
94        }
95    }
96
97    pub fn with_account_source(tag: NoteTag, account_id: AccountId) -> Self {
98        Self {
99            tag,
100            source: NoteTagSource::Account(account_id),
101        }
102    }
103}
104
105impl Serializable for NoteTagRecord {
106    fn write_into<W: ByteWriter>(&self, target: &mut W) {
107        self.tag.write_into(target);
108        self.source.write_into(target);
109    }
110}
111
112impl Deserializable for NoteTagRecord {
113    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
114        let tag = NoteTag::read_from(source)?;
115        let source = NoteTagSource::read_from(source)?;
116        Ok(Self { tag, source })
117    }
118}
119
120impl Serializable for NoteTagSource {
121    fn write_into<W: ByteWriter>(&self, target: &mut W) {
122        match self {
123            NoteTagSource::Account(account_id) => {
124                target.write_u8(0);
125                account_id.write_into(target);
126            },
127            NoteTagSource::Note(details_commitment) => {
128                target.write_u8(1);
129                details_commitment.write_into(target);
130            },
131            NoteTagSource::User => target.write_u8(2),
132            NoteTagSource::Subscription(key) => {
133                // Discriminant 3 must stay stable so rows survive deserialization.
134                target.write_u8(3);
135                key.write_into(target);
136            },
137        }
138    }
139}
140
141impl Deserializable for NoteTagSource {
142    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
143        match source.read_u8()? {
144            0 => Ok(NoteTagSource::Account(AccountId::read_from(source)?)),
145            1 => Ok(NoteTagSource::Note(NoteDetailsCommitment::read_from(source)?)),
146            2 => Ok(NoteTagSource::User),
147            3 => Ok(NoteTagSource::Subscription(Word::read_from(source)?)),
148            val => Err(DeserializationError::InvalidValue(format!("Invalid tag source: {val}"))),
149        }
150    }
151}
152
153impl PartialEq<NoteTag> for NoteTagRecord {
154    fn eq(&self, other: &NoteTag) -> bool {
155        self.tag == *other
156    }
157}
158
159impl From<&Account> for NoteTagRecord {
160    fn from(account: &Account) -> Self {
161        NoteTagRecord::with_account_source(NoteTag::with_account_target(account.id()), account.id())
162    }
163}
164
165impl TryInto<NoteTagRecord> for &InputNoteRecord {
166    type Error = NoteRecordError;
167
168    fn try_into(self) -> Result<NoteTagRecord, Self::Error> {
169        match self.metadata() {
170            Some(metadata) => {
171                Ok(NoteTagRecord::with_note_source(metadata.tag(), self.details_commitment()))
172            },
173            None => Err(NoteRecordError::ConversionError(
174                "Input Note Record does not contain tag".to_string(),
175            )),
176        }
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use miden_protocol::{Felt, Word};
183    use miden_tx::utils::serde::{Deserializable, Serializable};
184
185    use super::NoteTagSource;
186
187    #[test]
188    fn subscription_note_tag_source_round_trips_with_stable_discriminant() {
189        let key: Word =
190            [Felt::from(1u32), Felt::from(2u32), Felt::from(3u32), Felt::from(4u32)].into();
191        let source = NoteTagSource::Subscription(key);
192
193        let bytes = source.to_bytes();
194        // Discriminant byte must stay 3 so persisted rows keep deserializing across releases.
195        assert_eq!(bytes[0], 3, "Subscription discriminant must remain 3");
196        assert_eq!(NoteTagSource::read_from_bytes(&bytes).unwrap(), source);
197    }
198}