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