aleph_types/message/
base_message.rs

1use crate::chain::{Address, Chain, Signature};
2use crate::channel::Channel;
3use crate::item_hash::ItemHash;
4use crate::message::aggregate::AggregateContent;
5use crate::message::forget::ForgetContent;
6use crate::message::instance::InstanceContent;
7use crate::message::post::PostContent;
8use crate::message::program::ProgramContent;
9use crate::message::store::StoreContent;
10use crate::timestamp::Timestamp;
11use serde::de::{self, Deserializer};
12use serde::{Deserialize, Serialize};
13use std::fmt::Formatter;
14
15#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
16#[serde(rename_all = "UPPERCASE")]
17pub enum MessageType {
18    Aggregate,
19    Forget,
20    Instance,
21    Post,
22    Program,
23    Store,
24}
25
26impl std::fmt::Display for MessageType {
27    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
28        let s = match self {
29            MessageType::Aggregate => "AGGREGATE",
30            MessageType::Forget => "FORGET",
31            MessageType::Instance => "INSTANCE",
32            MessageType::Post => "POST",
33            MessageType::Program => "PROGRAM",
34            MessageType::Store => "STORE",
35        };
36
37        f.write_str(s)
38    }
39}
40
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42#[serde(rename_all = "lowercase")]
43pub enum MessageStatus {
44    Pending,
45    Processed,
46    Removing,
47    Removed,
48    Forgotten,
49    Rejected,
50}
51
52impl std::fmt::Display for MessageStatus {
53    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
54        let s = match self {
55            MessageStatus::Pending => "pending",
56            MessageStatus::Processed => "processed",
57            MessageStatus::Removing => "removing",
58            MessageStatus::Removed => "removed",
59            MessageStatus::Forgotten => "forgotten",
60            MessageStatus::Rejected => "rejected",
61        };
62
63        f.write_str(s)
64    }
65}
66
67/// Content variants for different message types.
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[serde(untagged)]
70pub enum MessageContentEnum {
71    Aggregate(AggregateContent),
72    Forget(ForgetContent),
73    Instance(InstanceContent),
74    Post(PostContent),
75    Program(ProgramContent),
76    Store(StoreContent),
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct MessageContent {
81    pub address: Address,
82    pub time: Timestamp,
83    #[serde(flatten)]
84    content: MessageContentEnum,
85}
86
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct MessageConfirmation {
89    pub chain: Chain,
90    pub height: u64,
91    pub hash: String,
92    pub time: Option<Timestamp>,
93    pub publisher: Option<Address>,
94}
95
96/// Where to find the content of the message. Note that this is a mix of ItemType / ItemContent
97/// if you are used to the Python SDK.
98#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
99#[serde(rename_all = "lowercase")]
100pub enum ContentSource {
101    Inline { item_content: String },
102    Storage,
103    Ipfs,
104}
105
106impl<'de> Deserialize<'de> for ContentSource {
107    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108    where
109        D: Deserializer<'de>,
110    {
111        #[derive(Deserialize)]
112        struct ContentSourceRaw {
113            item_type: String,
114            item_content: Option<String>,
115        }
116
117        let raw = ContentSourceRaw::deserialize(deserializer)?;
118
119        match raw.item_type.as_str() {
120            "inline" => {
121                let item_content = raw
122                    .item_content
123                    .ok_or_else(|| de::Error::missing_field("item_content"))?;
124                Ok(ContentSource::Inline { item_content })
125            }
126            "storage" => Ok(ContentSource::Storage),
127            "ipfs" => Ok(ContentSource::Ipfs),
128            other => Err(de::Error::unknown_variant(
129                other,
130                &["inline", "storage", "ipfs"],
131            )),
132        }
133    }
134}
135
136#[derive(PartialEq, Debug, Clone)]
137pub struct Message {
138    /// Blockchain used for this message.
139    pub chain: Chain,
140    /// Sender address.
141    pub sender: Address,
142    /// Cryptographic signature of the message by the sender.
143    pub signature: Signature,
144    /// Content of the message as created by the sender. Can either be inline or stored
145    /// on Aleph Cloud.
146    pub content_source: ContentSource,
147    /// Hash of the content (SHA2-256).
148    pub item_hash: ItemHash,
149    /// List of confirmations for the message.
150    pub confirmations: Vec<MessageConfirmation>,
151    /// Unix timestamp or datetime when the message was published.
152    pub time: Timestamp,
153    /// Channel of the message, one application ideally has one channel.
154    pub channel: Option<Channel>,
155    /// Message type. (aggregate, forget, instance, post, program, store).
156    pub message_type: MessageType,
157    /// Message content.
158    pub content: MessageContent,
159}
160
161impl Message {
162    pub fn content(&self) -> &MessageContentEnum {
163        &self.content.content
164    }
165
166    pub fn confirmed(&self) -> bool {
167        !self.confirmations.is_empty()
168    }
169
170    /// Returns the address of the sender of the message. Note that the sender is not necessarily
171    /// the owner of the resources, as the owner may have delegated their authority to create
172    /// specific resources through the permission system.
173    pub fn sender(&self) -> &Address {
174        &self.sender
175    }
176
177    /// Returns the address of the owner of the resources.
178    pub fn owner(&self) -> &Address {
179        &self.content.address
180    }
181
182    /// Returns the time at which the message was sent.
183    /// Notes:
184    /// * This value is signed by the sender and should not be trusted accordingly.
185    /// * We prefer `content.time` over `time` as `time` is not part of the signed payload.
186    pub fn sent_at(&self) -> &Timestamp {
187        &self.content.time
188    }
189
190    /// Returns the earliest confirmation time of the message.
191    pub fn confirmed_at(&self) -> Option<&Timestamp> {
192        self.confirmations.first().and_then(|c| c.time.as_ref())
193    }
194}
195
196// Custom deserializer that uses message_type to efficiently deserialize content
197impl<'de> Deserialize<'de> for Message {
198    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
199    where
200        D: Deserializer<'de>,
201    {
202        #[derive(Deserialize)]
203        struct MessageRaw {
204            chain: Chain,
205            sender: Address,
206            signature: Signature,
207            #[serde(flatten)]
208            content_source: ContentSource,
209            item_hash: ItemHash,
210            #[serde(default)]
211            confirmations: Option<Vec<MessageConfirmation>>,
212            time: Timestamp,
213            #[serde(default)]
214            channel: Option<Channel>,
215            #[serde(rename = "type")]
216            message_type: MessageType,
217            content: serde_json::Value,
218        }
219
220        let raw = MessageRaw::deserialize(deserializer)?;
221
222        // Deserialize content based on message_type for efficiency
223        let content_obj = raw
224            .content
225            .as_object()
226            .ok_or_else(|| de::Error::custom("content must be an object"))?;
227
228        let address = content_obj
229            .get("address")
230            .ok_or_else(|| de::Error::missing_field("content.address"))
231            .and_then(|v| Address::deserialize(v).map_err(de::Error::custom))?;
232
233        let time = content_obj
234            .get("time")
235            .ok_or_else(|| de::Error::missing_field("content.time"))
236            .and_then(|v| Timestamp::deserialize(v).map_err(de::Error::custom))?;
237
238        // Deserialize the specific variant based on message_type
239        let variant = match raw.message_type {
240            MessageType::Aggregate => MessageContentEnum::Aggregate(
241                AggregateContent::deserialize(&raw.content).map_err(de::Error::custom)?,
242            ),
243            MessageType::Forget => MessageContentEnum::Forget(
244                ForgetContent::deserialize(&raw.content).map_err(de::Error::custom)?,
245            ),
246            MessageType::Instance => MessageContentEnum::Instance(
247                InstanceContent::deserialize(&raw.content).map_err(de::Error::custom)?,
248            ),
249            MessageType::Post => MessageContentEnum::Post(
250                PostContent::deserialize(&raw.content).map_err(de::Error::custom)?,
251            ),
252            MessageType::Program => MessageContentEnum::Program(
253                ProgramContent::deserialize(&raw.content).map_err(de::Error::custom)?,
254            ),
255            MessageType::Store => MessageContentEnum::Store(
256                StoreContent::deserialize(&raw.content).map_err(de::Error::custom)?,
257            ),
258        };
259
260        Ok(Message {
261            chain: raw.chain,
262            sender: raw.sender,
263            signature: raw.signature,
264            content_source: raw.content_source,
265            item_hash: raw.item_hash,
266            confirmations: raw.confirmations.unwrap_or_default(),
267            time: raw.time,
268            channel: raw.channel,
269            message_type: raw.message_type,
270            content: MessageContent {
271                address,
272                time,
273                content: variant,
274            },
275        })
276    }
277}
278
279// Manual Serialize for Message
280impl Serialize for Message {
281    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
282    where
283        S: serde::Serializer,
284    {
285        use serde::ser::SerializeStruct;
286
287        let mut state = serializer.serialize_struct("Message", 9)?;
288        state.serialize_field("chain", &self.chain)?;
289        state.serialize_field("sender", &self.sender)?;
290        match &self.content_source {
291            ContentSource::Inline { item_content } => {
292                state.serialize_field("item_type", "inline")?;
293                state.serialize_field("item_content", item_content)?;
294            }
295            ContentSource::Storage => {
296                state.serialize_field("item_type", "storage")?;
297                state.serialize_field("item_content", &None::<String>)?;
298            }
299            ContentSource::Ipfs => {
300                state.serialize_field("item_type", "ipfs")?;
301                state.serialize_field("item_content", &None::<String>)?;
302            }
303        }
304        state.serialize_field("signature", &self.signature)?;
305        state.serialize_field("item_hash", &self.item_hash)?;
306        if self.confirmed() {
307            state.serialize_field("confirmed", &true)?;
308            state.serialize_field("confirmations", &self.confirmations)?;
309        }
310        state.serialize_field("time", &self.time)?;
311        if self.channel.is_some() {
312            state.serialize_field("channel", &self.channel)?;
313        }
314        state.serialize_field("type", &self.message_type)?;
315        state.serialize_field("content", &self.content)?;
316        state.end()
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use assert_matches::assert_matches;
324
325    #[test]
326    fn test_deserialize_item_type_inline() {
327        let item_content_str = "test".to_string();
328        let content_source_str =
329            format!("{{\"item_type\":\"inline\",\"item_content\":\"{item_content_str}\"}}");
330        let content_source: ContentSource = serde_json::from_str(&content_source_str).unwrap();
331
332        assert_matches!(
333            content_source,
334            ContentSource::Inline {
335                item_content
336            } if item_content == item_content_str
337        );
338    }
339
340    #[test]
341    fn test_deserialize_item_type_storage() {
342        let content_source_str = r#"{"item_type":"storage"}"#;
343        let content_source: ContentSource = serde_json::from_str(content_source_str).unwrap();
344        assert_matches!(content_source, ContentSource::Storage);
345    }
346
347    #[test]
348    fn test_deserialize_item_type_ipfs() {
349        let content_source_str = r#"{"item_type":"ipfs"}"#;
350        let content_source: ContentSource = serde_json::from_str(content_source_str).unwrap();
351        assert_matches!(content_source, ContentSource::Ipfs);
352    }
353
354    #[test]
355    fn test_deserialize_item_type_invalid_type() {
356        let content_source_str = r#"{"item_type":"invalid"}"#;
357        let result = serde_json::from_str::<ContentSource>(content_source_str);
358        assert!(result.is_err());
359    }
360
361    #[test]
362    fn test_deserialize_item_type_invalid_format() {
363        let content_source_str = r#"{"type":"inline"}"#;
364        let result = serde_json::from_str::<ContentSource>(content_source_str);
365        assert!(result.is_err());
366    }
367}