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