Skip to main content

xmtp_proto/types/
topic.rs

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