1use crate::chain::Address;
2use crate::cid::Cid;
3use crate::item_hash::{AlephItemHash, ItemHash};
4use crate::message::execution::base::{Payment, PaymentType};
5use memsizes::Bytes;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fmt::{Display, Formatter};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(tag = "item_type", rename_all = "lowercase")]
12pub enum StorageBackend {
13 Ipfs { item_hash: Cid },
14 Storage { item_hash: AlephItemHash },
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum StorageEngine {
23 Storage,
24 Ipfs,
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[serde(untagged)]
29pub enum RawFileRef {
32 ItemHash(ItemHash),
33 UserDefined(String),
34}
35
36impl Display for RawFileRef {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 RawFileRef::ItemHash(hash) => write!(f, "{}", hash),
40 RawFileRef::UserDefined(name) => write!(f, "{}", name),
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq)]
46pub enum FileRef {
50 ItemHash(ItemHash),
51 UserDefined { owner: Address, reference: String },
52}
53
54impl Display for FileRef {
55 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56 match self {
57 FileRef::ItemHash(item_hash) => write!(f, "{}", item_hash),
58 FileRef::UserDefined { owner, reference } => write!(f, "{}/{}", owner, reference),
59 }
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64pub struct StoreContent {
65 #[serde(flatten)]
66 file_hash: StorageBackend,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub size: Option<Bytes>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub content_type: Option<String>,
75 #[serde(rename = "ref", default, skip_serializing_if = "Option::is_none")]
76 pub reference: Option<RawFileRef>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub metadata: Option<HashMap<String, serde_json::Value>>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub payment: Option<Payment>,
83}
84
85impl StoreContent {
86 pub fn new(
87 file_hash: StorageBackend,
88 reference: Option<RawFileRef>,
89 metadata: Option<HashMap<String, serde_json::Value>>,
90 payment: Option<Payment>,
91 ) -> Self {
92 Self {
93 file_hash,
94 size: None,
95 content_type: None,
96 reference,
97 metadata,
98 payment,
99 }
100 }
101
102 pub fn has_valid_payment(&self) -> bool {
105 match &self.payment {
106 None => true,
107 Some(p) => !matches!(p.payment_type, PaymentType::Superfluid),
108 }
109 }
110
111 pub fn file_hash(&self) -> ItemHash {
112 match &self.file_hash {
113 StorageBackend::Ipfs { item_hash: cid } => ItemHash::Ipfs(cid.clone()),
114 StorageBackend::Storage { item_hash } => ItemHash::Native(*item_hash),
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::chain::{Address, Chain, Signature};
123 use crate::channel::Channel;
124 use crate::item_hash;
125 use crate::message::base_message::{Message, MessageContentEnum};
126 use crate::message::{ContentSource, MessageType};
127 use crate::timestamp::Timestamp;
128 use assert_matches::assert_matches;
129
130 const STORE_IPFS_FIXTURE: &str = include_str!(concat!(
131 env!("CARGO_MANIFEST_DIR"),
132 "/../../fixtures/messages/store/store-ipfs.json"
133 ));
134
135 #[test]
136 fn test_deserialize_store_message() {
137 let message: Message = serde_json::from_str(STORE_IPFS_FIXTURE).unwrap();
138
139 assert_eq!(
140 message.sender,
141 Address::from("0x238224C744F4b90b4494516e074D2676ECfC6803".to_string())
142 );
143 assert_eq!(message.chain, Chain::Ethereum);
144 assert_eq!(
145 message.signature,
146 Some(Signature::from(
147 "0x9c87f5d659b9165be7cbd4b9f0bd5df5c66b9bb9a384a0a33b1277428be21244595a0731697035c4b085064cd3fc088bc5b3cddeb22159e7f462e6e5b5e7e8181c".to_string()
148 ))
149 );
150 assert_matches!(message.message_type, MessageType::Store);
151 assert_matches!(
152 message.content_source,
153 ContentSource::Inline { item_content: _ }
154 );
155 assert_eq!(
156 &message.item_hash.to_string(),
157 "afe106f1fd70b6b806e0452cc2f9485e518143581ffd046ae19fc64af7b6bbaa"
158 );
159 assert_eq!(message.time, Timestamp::from(1761047957.74837));
160 assert_matches!(message.channel, Some(ref channel) if channel == &Channel::from("ALEPH-CLOUDSOLUTIONS".to_string()));
161
162 assert_eq!(
164 &message.content.address,
165 &Address::from("0x238224C744F4b90b4494516e074D2676ECfC6803".to_string())
166 );
167 assert_eq!(&message.content.time, &Timestamp::from(1761047957.7483068));
168 assert_eq!(message.sent_at(), &message.content.time);
169
170 match message.content() {
172 MessageContentEnum::Store(store) => {
173 assert_eq!(
174 store.file_hash,
175 StorageBackend::Ipfs {
176 item_hash: Cid::try_from("QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8")
177 .unwrap()
178 }
179 );
180 assert_eq!(
181 store.file_hash(),
182 item_hash!("QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8")
183 );
184
185 assert!(store.size.is_none());
186 assert!(store.content_type.is_none());
187 assert!(store.reference.is_none());
188 assert!(store.metadata.is_none());
189 }
190 other => {
191 panic!("Expected MessageContentEnum::Store, got {:?}", other)
192 }
193 }
194
195 assert!(message.confirmed());
197 assert_eq!(message.confirmations.len(), 1);
198
199 let confirmation = &message.confirmations[0];
200 assert_eq!(confirmation.chain, Chain::Ethereum);
201 assert_eq!(confirmation.height, 23626206);
202 assert_eq!(
203 confirmation.hash,
204 "0x7e73ff97d7920fcfc289a899aeac4bc2898d1482a9876bd2ac4584ae876d22be"
205 );
206 assert!(confirmation.time.is_none());
207 assert!(confirmation.publisher.is_none());
208
209 message.verify_item_hash().unwrap();
210 }
211
212 #[test]
213 fn test_deserialize_serialized_store_message() {
214 let message: Message = serde_json::from_str(STORE_IPFS_FIXTURE).unwrap();
215 message.verify_item_hash().unwrap();
216
217 let serialized_message = serde_json::to_string(&message).unwrap();
218 let deserialized_message: Message = serde_json::from_str(&serialized_message).unwrap();
219 deserialized_message.verify_item_hash().unwrap();
220
221 assert_eq!(message, deserialized_message);
222 }
223
224 const TEST_HASH: &str = "d281eb8a69ba1f4dda2d71aaf3ded06caa92edd690ef3d0632f41aa91167762c";
225
226 #[test]
227 fn test_store_content_without_payment() {
228 let json = format!(r#"{{"item_type":"storage","item_hash":"{}"}}"#, TEST_HASH);
229 let content: StoreContent = serde_json::from_str(&json).unwrap();
230 assert!(content.payment.is_none());
231 assert!(content.has_valid_payment());
232 }
233
234 #[test]
235 fn test_store_content_with_credit_payment() {
236 let json = format!(
237 r#"{{"item_type":"storage","item_hash":"{}","payment":{{"type":"credit"}}}}"#,
238 TEST_HASH
239 );
240 let content: StoreContent = serde_json::from_str(&json).unwrap();
241 let payment = content.payment.as_ref().unwrap();
242 assert_eq!(payment.payment_type, PaymentType::Credit);
243 assert!(payment.chain.is_none());
244 assert!(payment.receiver.is_none());
245 assert!(content.has_valid_payment());
246 }
247
248 #[test]
249 fn test_store_content_with_hold_payment() {
250 let json = format!(
251 r#"{{"item_type":"storage","item_hash":"{}","payment":{{"type":"hold"}}}}"#,
252 TEST_HASH
253 );
254 let content: StoreContent = serde_json::from_str(&json).unwrap();
255 assert_eq!(
256 content.payment.as_ref().unwrap().payment_type,
257 PaymentType::Hold
258 );
259 assert!(content.has_valid_payment());
260 }
261
262 #[test]
263 fn test_store_content_superfluid_payment_invalid() {
264 let json = format!(
265 r#"{{"item_type":"storage","item_hash":"{}","payment":{{"type":"superfluid"}}}}"#,
266 TEST_HASH
267 );
268 let content: StoreContent = serde_json::from_str(&json).unwrap();
269 assert!(!content.has_valid_payment());
270 }
271
272 #[test]
273 fn test_store_content_credit_payment_round_trip() {
274 let json = format!(
275 r#"{{"item_type":"storage","item_hash":"{}","payment":{{"type":"credit"}}}}"#,
276 TEST_HASH
277 );
278 let content: StoreContent = serde_json::from_str(&json).unwrap();
279 let serialized = serde_json::to_string(&content).unwrap();
280 let deserialized: StoreContent = serde_json::from_str(&serialized).unwrap();
281 assert_eq!(content, deserialized);
282 }
283
284 #[test]
285 fn test_store_content_payment_not_serialized_when_none() {
286 let content = StoreContent::new(
287 StorageBackend::Storage {
288 item_hash: AlephItemHash::from_bytes(b"test"),
289 },
290 None,
291 None,
292 None,
293 );
294 let json = serde_json::to_string(&content).unwrap();
295 assert!(!json.contains("payment"));
296 }
297}