Skip to main content

aleph_types/message/
store.rs

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