Skip to main content

aleph_types/message/
store.rs

1use crate::chain::Address;
2use crate::cid::Cid;
3use crate::item_hash::{AlephItemHash, ItemHash};
4use crate::memory_size::Bytes;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fmt::{Display, Formatter};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(tag = "item_type", rename_all = "lowercase")]
11pub enum StorageBackend {
12    Ipfs { item_hash: Cid },
13    Storage { item_hash: AlephItemHash },
14}
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[serde(untagged)]
18/// File reference, as deserialized in STORE messages. Does not contain
19/// information about the owner.
20pub enum RawFileRef {
21    ItemHash(ItemHash),
22    UserDefined(String),
23}
24
25impl Display for RawFileRef {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            RawFileRef::ItemHash(hash) => write!(f, "{}", hash),
29            RawFileRef::UserDefined(name) => write!(f, "{}", name),
30        }
31    }
32}
33
34#[derive(Debug, Clone, PartialEq)]
35/// Explicit file reference, tagged with the owner address for the user-defined case.
36/// This is required to perform unambiguous queries on user-defined file references
37/// as multiple users may use the same references.
38pub enum FileRef {
39    ItemHash(ItemHash),
40    UserDefined { owner: Address, reference: String },
41}
42
43impl Display for FileRef {
44    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
45        match self {
46            FileRef::ItemHash(item_hash) => write!(f, "{}", item_hash),
47            FileRef::UserDefined { owner, reference } => write!(f, "{}/{}", owner, reference),
48        }
49    }
50}
51
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct StoreContent {
54    #[serde(flatten)]
55    /// A combination of the `item_hash` and `item_type` fields, deserialized together to detect
56    /// inconsistencies. Use the `file_hash()` method to access the file hash.
57    file_hash: StorageBackend,
58    #[serde(default)]
59    /// Size of the file. Generated by CCNs upon processing.
60    pub size: Option<Bytes>,
61    /// Generated by CCNs upon processing.
62    pub content_type: Option<String>,
63    #[serde(rename = "ref", default)]
64    pub reference: Option<RawFileRef>,
65    /// Metadata of the VM.
66    pub metadata: Option<HashMap<String, serde_json::Value>>,
67}
68
69impl StoreContent {
70    pub fn file_hash(&self) -> ItemHash {
71        match &self.file_hash {
72            StorageBackend::Ipfs { item_hash: cid } => ItemHash::Ipfs(cid.clone()),
73            StorageBackend::Storage { item_hash } => ItemHash::Native(*item_hash),
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::chain::{Address, Chain, Signature};
82    use crate::channel::Channel;
83    use crate::item_hash;
84    use crate::message::base_message::{Message, MessageContentEnum};
85    use crate::message::{ContentSource, MessageType};
86    use crate::timestamp::Timestamp;
87    use assert_matches::assert_matches;
88
89    const STORE_IPFS_FIXTURE: &str = include_str!(concat!(
90        env!("CARGO_MANIFEST_DIR"),
91        "/../../fixtures/messages/store/store-ipfs.json"
92    ));
93
94    #[test]
95    fn test_deserialize_store_message() {
96        let message: Message = serde_json::from_str(STORE_IPFS_FIXTURE).unwrap();
97
98        assert_eq!(
99            message.sender,
100            Address::from("0x238224C744F4b90b4494516e074D2676ECfC6803".to_string())
101        );
102        assert_eq!(message.chain, Chain::Ethereum);
103        assert_eq!(
104                message.signature,
105                Signature::from(
106                    "0x9c87f5d659b9165be7cbd4b9f0bd5df5c66b9bb9a384a0a33b1277428be21244595a0731697035c4b085064cd3fc088bc5b3cddeb22159e7f462e6e5b5e7e8181c".to_string()
107                )
108            );
109        assert_matches!(message.message_type, MessageType::Store);
110        assert_matches!(
111            message.content_source,
112            ContentSource::Inline { item_content: _ }
113        );
114        assert_eq!(
115            &message.item_hash.to_string(),
116            "afe106f1fd70b6b806e0452cc2f9485e518143581ffd046ae19fc64af7b6bbaa"
117        );
118        assert_eq!(message.time, Timestamp::from(1761047957.74837));
119        assert_matches!(message.channel, Some(ref channel) if channel == &Channel::from("ALEPH-CLOUDSOLUTIONS".to_string()));
120
121        // Check content fields
122        assert_eq!(
123            &message.content.address,
124            &Address::from("0x238224C744F4b90b4494516e074D2676ECfC6803".to_string())
125        );
126        assert_eq!(&message.content.time, &Timestamp::from(1761047957.7483068));
127        assert_eq!(message.sent_at(), &message.content.time);
128
129        // Check STORE-specific fields
130        match message.content() {
131            MessageContentEnum::Store(store) => {
132                assert_eq!(
133                    store.file_hash,
134                    StorageBackend::Ipfs {
135                        item_hash: Cid::try_from("QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8")
136                            .unwrap()
137                    }
138                );
139                assert_eq!(
140                    store.file_hash(),
141                    item_hash!("QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8")
142                );
143
144                assert!(store.size.is_none());
145                assert!(store.content_type.is_none());
146                assert!(store.reference.is_none());
147                assert!(store.metadata.is_none());
148            }
149            other => {
150                panic!("Expected MessageContentEnum::Store, got {:?}", other)
151            }
152        }
153
154        // Check confirmations
155        assert!(message.confirmed());
156        assert_eq!(message.confirmations.len(), 1);
157
158        let confirmation = &message.confirmations[0];
159        assert_eq!(confirmation.chain, Chain::Ethereum);
160        assert_eq!(confirmation.height, 23626206);
161        assert_eq!(
162            confirmation.hash,
163            "0x7e73ff97d7920fcfc289a899aeac4bc2898d1482a9876bd2ac4584ae876d22be"
164        );
165        assert!(confirmation.time.is_none());
166        assert!(confirmation.publisher.is_none());
167    }
168
169    #[test]
170    fn test_deserialize_serialized_store_message() {
171        let message: Message = serde_json::from_str(STORE_IPFS_FIXTURE).unwrap();
172        let serialized_message = serde_json::to_string(&message).unwrap();
173        let deserialized_message: Message = serde_json::from_str(&serialized_message).unwrap();
174
175        assert_eq!(message, deserialized_message);
176    }
177}