xmtp_proto/types/
topic.rs

1use std::{
2    fmt::{Debug, Display},
3    ops::Deref,
4};
5
6use smallvec::SmallVec;
7
8use crate::{ConversionError, types::InstallationId, xmtp::xmtpv4::envelopes::AuthenticatedData};
9
10/// the max size of an item in a [`TopicKind`] is 32 bytes (installation id).
11/// the 1st byte is interpreted as the prefixed [`TopicKind`] byte.
12type TopicBytes = SmallVec<[u8; 33]>;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[repr(u8)]
16#[non_exhaustive]
17pub enum TopicKind {
18    GroupMessagesV1 = 0,
19    WelcomeMessagesV1 = 1,
20    IdentityUpdatesV1 = 2,
21    KeyPackagesV1 = 3,
22}
23
24impl TryFrom<u8> for TopicKind {
25    type Error = crate::ConversionError;
26
27    fn try_from(value: u8) -> Result<Self, Self::Error> {
28        match value {
29            0 => Ok(TopicKind::GroupMessagesV1),
30            1 => Ok(TopicKind::WelcomeMessagesV1),
31            2 => Ok(TopicKind::IdentityUpdatesV1),
32            3 => Ok(TopicKind::KeyPackagesV1),
33            i => Err(ConversionError::InvalidValue {
34                item: "u8",
35                expected: "an unsigned integer in the range 0-3",
36                got: i.to_string(),
37            }),
38        }
39    }
40}
41
42impl Display for TopicKind {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        use TopicKind::*;
45        match self {
46            GroupMessagesV1 => write!(f, "group_message_v1"),
47            WelcomeMessagesV1 => write!(f, "welcome_message_v1"),
48            IdentityUpdatesV1 => write!(f, "identity_updates_v1"),
49            KeyPackagesV1 => write!(f, "key_packages_v1"),
50        }
51    }
52}
53
54impl TopicKind {
55    fn build<B: AsRef<[u8]>>(&self, bytes: B) -> TopicBytes {
56        let bytes = bytes.as_ref();
57        let mut topic = TopicBytes::new();
58        topic.push(*self as u8);
59        topic.extend_from_slice(bytes);
60        topic
61    }
62
63    pub fn create<B: AsRef<[u8]>>(&self, bytes: B) -> Topic {
64        Topic {
65            inner: self.build(bytes),
66        }
67    }
68}
69
70/// A topic where the first byte is the kind
71/// https://github.com/xmtp/XIPs/blob/main/XIPs/xip-49-decentralized-backend.md#332-envelopes
72#[derive(Clone, PartialEq, Eq, Hash)]
73pub struct Topic {
74    inner: TopicBytes,
75}
76
77impl Topic {
78    pub fn new(kind: TopicKind, bytes: Vec<u8>) -> Self {
79        Self {
80            inner: kind.build(bytes),
81        }
82    }
83
84    /// create a new [`TopicKind::GroupMessagesV1`] topic
85    pub fn new_group_message(group_id: impl AsRef<[u8]>) -> Self {
86        TopicKind::GroupMessagesV1.create(group_id)
87    }
88
89    /// create a new identity update Topic with `inbox_id` bytes
90    /// _NOTE_
91    /// this function expects the decoded hex from an InboxId,
92    /// not the UTF-8 bytes of a InboxId.
93    pub fn new_identity_update(inbox_id: impl AsRef<[u8]>) -> Self {
94        TopicKind::IdentityUpdatesV1.create(inbox_id)
95    }
96
97    /// create a new [`TopicKind::WelcomeMessagesV1`] topic
98    /// from an [`InstallationId`]
99    pub fn new_welcome_message(installation_id: InstallationId) -> Self {
100        TopicKind::WelcomeMessagesV1.create(installation_id)
101    }
102
103    /// create a new [`TopicKind::KeyPackagesV1`] topic
104    /// from an [`InstallationId`]
105    pub fn new_key_package(installation_id: impl AsRef<[u8]>) -> Self {
106        TopicKind::KeyPackagesV1.create(installation_id.as_ref())
107    }
108
109    pub fn kind(&self) -> TopicKind {
110        self.inner[0]
111            .try_into()
112            .expect("A topic must always be built with a valid `TopicKind`")
113    }
114
115    /// Get only the identifying portion of this topic
116    pub fn identifier(&self) -> &[u8] {
117        &self.inner[1..]
118    }
119
120    /// get the full topic bytes as a [`Vec`] by cloning, including the identifying [`TopicKind`]
121    pub fn cloned_vec(&self) -> Vec<u8> {
122        self.inner.clone().to_vec()
123    }
124
125    /// consume this [`Topic`] into its bytes as a Vec
126    pub fn to_bytes(self) -> TopicBytes {
127        self.inner
128    }
129
130    /// treat this topic as a [`TopicKind::IdentityUpdatesV1`],
131    /// otherwise returns [`Option::None`].
132    /// useful for collection `filter_map` operations when a single topic type
133    /// is required
134    pub fn identity_updates(&self) -> Option<&Topic> {
135        if self.kind() == TopicKind::IdentityUpdatesV1 {
136            Some(self)
137        } else {
138            None
139        }
140    }
141
142    /// treat this topic as a [`TopicKind::GroupMessagesV1`],
143    /// otherwise returns [`Option::None`].
144    /// useful for collection `filter_map` operations when a single topic type
145    /// is required
146    pub fn group_message_v1(&self) -> Option<&Topic> {
147        if self.kind() == TopicKind::GroupMessagesV1 {
148            Some(self)
149        } else {
150            None
151        }
152    }
153
154    /// treat this topic as a [`TopicKind::WelcomeMessagesV1`],
155    /// otherwise returns [`Option::None`].
156    /// useful for collection `filter_map` operations when a single topic type
157    /// is required
158    pub fn welcome_message_v1(&self) -> Option<&Topic> {
159        if self.kind() == TopicKind::WelcomeMessagesV1 {
160            Some(self)
161        } else {
162            None
163        }
164    }
165
166    /// treat this topic as a [`TopicKind::KeyPackagesV1`],
167    /// otherwise returns [`Option::None`].
168    /// useful for collection `filter_map` operations when a single topic type
169    /// is required
170    pub fn key_packages_v1(&self) -> Option<&Topic> {
171        if self.kind() == TopicKind::KeyPackagesV1 {
172            Some(self)
173        } else {
174            None
175        }
176    }
177
178    /// create a topic from bytes
179    /// this is test only. using topics with
180    /// invalid byte layout will result in
181    /// undefined behavior.
182    #[cfg(any(feature = "test-utils", test))]
183    pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
184        Self {
185            inner: SmallVec::from_slice(bytes.as_ref()),
186        }
187    }
188}
189
190impl TryFrom<Vec<u8>> for Topic {
191    type Error = ConversionError;
192
193    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
194        if let Some(byte) = value.first() {
195            let kind = TopicKind::try_from(*byte)?;
196            Ok(Topic::new(kind, value[1..].to_vec()))
197        } else {
198            Err(ConversionError::InvalidValue {
199                item: "Topic",
200                expected: "a byte array where the first byte is a valid TopicKind",
201                got: hex::encode(value),
202            })
203        }
204    }
205}
206
207impl From<Topic> for Vec<u8> {
208    fn from(topic: Topic) -> Vec<u8> {
209        topic.to_bytes().to_vec()
210    }
211}
212
213impl Debug for Topic {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        f.debug_struct("Topic")
216            .field("kind", &self.kind())
217            .field("bytes", &hex::encode(self.identifier()))
218            .finish()
219    }
220}
221
222impl Display for Topic {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        write!(f, "[{}/{}]", self.kind(), hex::encode(self.identifier()))
225    }
226}
227
228impl Deref for Topic {
229    type Target = [u8];
230
231    fn deref(&self) -> &Self::Target {
232        self.inner.deref()
233    }
234}
235
236impl<T> AsRef<T> for Topic
237where
238    T: ?Sized,
239    <Topic as Deref>::Target: AsRef<T>,
240{
241    fn as_ref(&self) -> &T {
242        self.deref().as_ref()
243    }
244}
245
246impl AsRef<Topic> for Topic {
247    fn as_ref(&self) -> &Topic {
248        self
249    }
250}
251
252impl AuthenticatedData {
253    pub fn with_topic(topic: Topic) -> AuthenticatedData {
254        AuthenticatedData {
255            target_topic: topic.into(),
256            depends_on: None,
257        }
258    }
259}