Skip to main content

aleph_types/message/
pending.rs

1use crate::chain::{Address, Chain, Signature};
2use crate::channel::Channel;
3use crate::item_hash::ItemHash;
4use crate::message::item_type::ItemType;
5use crate::message::{ContentSource, Message, MessageType};
6use crate::timestamp::Timestamp;
7use serde::ser::SerializeStruct;
8use serde::{Serialize, Serializer};
9use thiserror::Error;
10
11/// A signed message ready for submission to the Aleph network.
12///
13/// The `item_content` field is always present in memory (needed for uploading
14/// storage/IPFS content), but the custom `Serialize` implementation only emits
15/// it when `item_type == Inline`.
16#[derive(Debug, Clone)]
17pub struct PendingMessage {
18    pub chain: Chain,
19    pub sender: Address,
20    pub signature: Signature,
21    pub message_type: MessageType,
22    pub item_type: ItemType,
23    pub item_content: String,
24    pub item_hash: ItemHash,
25    pub time: Timestamp,
26    pub channel: Option<Channel>,
27}
28
29impl Serialize for PendingMessage {
30    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
31        let has_content = self.item_type == ItemType::Inline;
32        let has_channel = self.channel.is_some();
33        let field_count = 7 + has_content as usize + has_channel as usize;
34
35        let mut state = serializer.serialize_struct("PendingMessage", field_count)?;
36        state.serialize_field("sender", &self.sender)?;
37        state.serialize_field("chain", &self.chain)?;
38        state.serialize_field("signature", &self.signature)?;
39        state.serialize_field("type", &self.message_type)?;
40        state.serialize_field("item_type", &self.item_type)?;
41        if has_content {
42            state.serialize_field("item_content", &self.item_content)?;
43        }
44        state.serialize_field("item_hash", &self.item_hash)?;
45        state.serialize_field("time", &self.time)?;
46        if let Some(channel) = &self.channel {
47            state.serialize_field("channel", channel)?;
48        }
49        state.end()
50    }
51}
52
53/// Reasons a [`Message`] cannot be converted into a [`PendingMessage`] for re-submission.
54#[derive(Error, Debug)]
55pub enum PendingConversionError {
56    /// The message has no signature (legacy null-signature mainnet data).
57    /// Such messages cannot be re-posted: the target node would reject them.
58    #[error("message has no signature; cannot be re-posted")]
59    MissingSignature,
60}
61
62impl TryFrom<&Message> for PendingMessage {
63    type Error = PendingConversionError;
64
65    fn try_from(message: &Message) -> Result<Self, Self::Error> {
66        let signature = message
67            .signature
68            .clone()
69            .ok_or(PendingConversionError::MissingSignature)?;
70        let (item_type, item_content) = match &message.content_source {
71            ContentSource::Inline { item_content } => (ItemType::Inline, item_content.clone()),
72            ContentSource::Storage => (ItemType::Storage, String::new()),
73            ContentSource::Ipfs => (ItemType::Ipfs, String::new()),
74        };
75        Ok(PendingMessage {
76            chain: message.chain.clone(),
77            sender: message.sender.clone(),
78            signature,
79            message_type: message.message_type,
80            item_type,
81            item_content,
82            item_hash: message.item_hash.clone(),
83            time: message.time.clone(),
84            channel: message.channel.clone(),
85        })
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::{address, item_hash};
93
94    fn make_pending(item_type: ItemType) -> PendingMessage {
95        PendingMessage {
96            chain: Chain::Ethereum,
97            sender: address!("0xABCD"),
98            signature: Signature::from("0xSIG".to_string()),
99            message_type: MessageType::Post,
100            item_type,
101            item_content: r#"{"type":"test","address":"0xABCD","time":1234.0}"#.to_string(),
102            item_hash: item_hash!(
103                "d281eb8a69ba1f4dda2d71aaf3ded06caa92edd690ef3d0632f41aa91167762c"
104            ),
105            time: Timestamp::from(1234.0),
106            channel: None,
107        }
108    }
109
110    #[test]
111    fn test_pending_message_inline_includes_item_content() {
112        let msg = make_pending(ItemType::Inline);
113        let json = serde_json::to_value(&msg).unwrap();
114        assert_eq!(json["item_type"], "inline");
115        assert!(json.get("item_content").is_some());
116        assert_eq!(json["item_content"], msg.item_content);
117    }
118
119    #[test]
120    fn test_pending_message_storage_omits_item_content() {
121        let msg = make_pending(ItemType::Storage);
122        let json = serde_json::to_value(&msg).unwrap();
123        assert_eq!(json["item_type"], "storage");
124        assert!(json.get("item_content").is_none());
125    }
126
127    #[test]
128    fn test_pending_message_ipfs_omits_item_content() {
129        let msg = make_pending(ItemType::Ipfs);
130        let json = serde_json::to_value(&msg).unwrap();
131        assert_eq!(json["item_type"], "ipfs");
132        assert!(json.get("item_content").is_none());
133    }
134}