rain_metadata/
metaboard.rs1use alloy::primitives::{Bytes, FixedBytes, hex};
2use alloy::sol_types::SolCall;
3use rain_metadata_bindings::IMetaBoardV1_2::emitMetaCall;
4use serde::Serialize;
5
6use crate::{Error, RainMetaDocumentV1Item};
7use crate::meta::types::dotrain::source_v1::DotrainSourceV1;
8
9fn generate_emit_data_calldata(subject: FixedBytes<32>, data: Vec<u8>) -> Vec<u8> {
11 let call = emitMetaCall {
12 subject,
13 meta: Bytes::from(data),
14 };
15 call.abi_encode()
16}
17
18pub fn generate_emit_meta_calldata(meta: RainMetaDocumentV1Item) -> Result<Vec<u8>, Error> {
25 let hash = meta.hash(false)?;
26 let meta_bytes = RainMetaDocumentV1Item::cbor_encode_seq(
27 &vec![meta],
28 crate::KnownMagic::RainMetaDocumentV1,
29 )?;
30 Ok(generate_emit_data_calldata(hash.into(), meta_bytes))
31}
32
33#[derive(Debug, Clone, Serialize, serde::Deserialize)]
35pub struct DotrainSourceEmitData {
36 pub subject: String,
38 pub meta_bytes: String,
40 pub calldata: String,
42}
43
44fn validate_dotrain_content(content: &str) -> Result<(), Error> {
46 if content.trim().is_empty() {
48 return Err(Error::InvalidInput(
49 "Dotrain content cannot be empty".to_string(),
50 ));
51 }
52 Ok(())
53}
54
55pub fn generate_dotrain_source_emit_tx_data(content: &str) -> Result<DotrainSourceEmitData, Error> {
58 validate_dotrain_content(content)?;
60
61 let dotrain_source = DotrainSourceV1(content.to_string());
62 let subject_hash = dotrain_source.hash();
63 let document: RainMetaDocumentV1Item = dotrain_source.into();
64 let documents = vec![document];
65
66 let meta_bytes =
68 RainMetaDocumentV1Item::cbor_encode_seq(&documents, crate::KnownMagic::RainMetaDocumentV1)?;
69
70 let meta_bytes_hex = hex::encode_prefixed(&meta_bytes);
72 let calldata = generate_emit_data_calldata(subject_hash, meta_bytes);
74
75 Ok(DotrainSourceEmitData {
76 subject: hex::encode_prefixed(subject_hash),
77 meta_bytes: meta_bytes_hex,
78 calldata: hex::encode_prefixed(calldata),
79 })
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::meta::{ContentEncoding, ContentLanguage, ContentType, KnownMagic};
86
87 fn create_test_meta(content: &str) -> RainMetaDocumentV1Item {
88 RainMetaDocumentV1Item {
89 payload: serde_bytes::ByteBuf::from(content),
90 magic: KnownMagic::DotrainSourceV1,
91 content_type: ContentType::OctetStream,
92 content_encoding: ContentEncoding::None,
93 content_language: ContentLanguage::None,
94 schema: None,
95 }
96 }
97
98 #[test]
99 fn test_generate_emit_meta_calldata_success() {
100 let meta = create_test_meta("test content");
101 let calldata = generate_emit_meta_calldata(meta.clone()).unwrap();
102
103 assert!(!calldata.is_empty());
105
106 let decoded = emitMetaCall::abi_decode(&calldata).unwrap();
108 let expected_hash = meta.hash(false).unwrap();
109 let expected_meta_bytes =
110 RainMetaDocumentV1Item::cbor_encode_seq(&vec![meta], KnownMagic::RainMetaDocumentV1)
111 .unwrap();
112
113 assert_eq!(decoded.subject, FixedBytes::from(expected_hash));
114 assert_eq!(decoded.meta.as_ref(), expected_meta_bytes.as_slice());
115 }
116
117 #[test]
121 fn test_emit_meta_bytes_are_a_rain_meta_document() {
122 let meta = create_test_meta("test content");
123 let calldata = generate_emit_meta_calldata(meta.clone()).unwrap();
124 let emitted = emitMetaCall::abi_decode(&calldata).unwrap().meta;
125
126 assert_eq!(hex::encode(&emitted[..8]), "ff0a89c674ee7874");
128
129 let decoded = RainMetaDocumentV1Item::cbor_decode(&emitted).unwrap();
131 assert_eq!(decoded.len(), 1);
132 assert_eq!(decoded[0], meta);
133
134 let bare = meta.cbor_encode().unwrap();
136 assert_ne!(emitted.as_ref(), bare.as_slice());
137 assert_eq!(&emitted[8..], bare.as_slice());
138 }
139
140 #[test]
141 fn test_validate_dotrain_content() {
142 assert!(validate_dotrain_content("#main _ _: int-add(1 2)").is_ok());
144 assert!(validate_dotrain_content("/* comment */\n#main _ _: 1").is_ok());
145
146 assert!(validate_dotrain_content("").is_err());
148 assert!(validate_dotrain_content(" ").is_err());
149 assert!(validate_dotrain_content("\n\t\r").is_err());
150 }
151
152 #[test]
153 fn test_generate_dotrain_source_emit_tx_data_success() {
154 let content = "#main _ _: int-add(1 2) int-add(2 3)";
155 let deployment = generate_dotrain_source_emit_tx_data(content).unwrap();
156
157 assert!(deployment.subject.starts_with("0x"));
159 assert!(deployment.meta_bytes.starts_with("0x"));
160 assert!(deployment.calldata.starts_with("0x"));
161
162 assert_eq!(deployment.subject.len(), 66); assert!(deployment.meta_bytes.len() > 10); assert!(deployment.calldata.len() > 10); let calldata_bytes =
169 alloy::hex::decode(deployment.calldata.trim_start_matches("0x")).unwrap();
170 let decoded = emitMetaCall::abi_decode(&calldata_bytes).unwrap();
171 let subject_hex = deployment.subject.to_lowercase();
172 assert_eq!(hex::encode_prefixed(decoded.subject), subject_hex);
173
174 let meta_bytes_from_field =
176 alloy::hex::decode(deployment.meta_bytes.trim_start_matches("0x")).unwrap();
177 assert_eq!(decoded.meta.as_ref(), meta_bytes_from_field.as_slice());
178
179 assert!(deployment.meta_bytes.starts_with("0xff0a89c674ee7874"));
182
183 let expected_subject = alloy::primitives::keccak256(content.as_bytes());
186 assert_eq!(deployment.subject, hex::encode_prefixed(expected_subject));
187 }
188
189 #[test]
190 fn test_emit_subject_is_the_fetch_by_subject_key() {
191 for content in ["x", "#main _ _: int-add(1 2)", "/* c */\n#main _: 1;"] {
196 let deployment = generate_dotrain_source_emit_tx_data(content).unwrap();
197 let source = DotrainSourceV1(content.to_string());
198
199 assert_eq!(deployment.subject, hex::encode_prefixed(source.hash()));
200
201 let document: RainMetaDocumentV1Item = source.into();
202 let item_map_hash = document.hash(false).unwrap();
203 assert_ne!(deployment.subject, hex::encode_prefixed(item_map_hash));
204 }
205 }
206
207 #[test]
208 fn test_generate_dotrain_source_emit_tx_data_empty_content() {
209 let result = generate_dotrain_source_emit_tx_data("");
210 assert!(result.is_err());
211
212 let result = generate_dotrain_source_emit_tx_data(" ");
213 assert!(result.is_err());
214 }
215
216 #[test]
217 fn test_cas_property_same_content() {
218 let meta1 = create_test_meta("identical content");
220 let meta2 = create_test_meta("identical content");
221
222 let calldata1 = generate_emit_meta_calldata(meta1).unwrap();
223 let calldata2 = generate_emit_meta_calldata(meta2).unwrap();
224
225 assert_eq!(calldata1, calldata2);
227 }
228
229 #[test]
230 fn test_cas_property_different_content() {
231 let meta1 = create_test_meta("content A");
232 let meta2 = create_test_meta("content B");
233
234 let calldata1 = generate_emit_meta_calldata(meta1).unwrap();
235 let calldata2 = generate_emit_meta_calldata(meta2).unwrap();
236
237 assert_ne!(calldata1, calldata2);
239
240 let decoded1 = emitMetaCall::abi_decode(&calldata1).unwrap();
242 let decoded2 = emitMetaCall::abi_decode(&calldata2).unwrap();
243 assert_ne!(decoded1.subject, decoded2.subject);
244 }
245
246 #[test]
247 fn test_different_magic_same_content() {
248 let meta1 = create_test_meta("same content");
249 let mut meta2 = create_test_meta("same content");
250 meta2.magic = KnownMagic::AuthoringMetaV1; let calldata1 = generate_emit_meta_calldata(meta1).unwrap();
253 let calldata2 = generate_emit_meta_calldata(meta2).unwrap();
254
255 assert_ne!(calldata1, calldata2);
257 }
258
259 #[test]
260 fn test_hash_deterministic() {
261 let meta = create_test_meta("test for deterministic hash");
262
263 let calldata1 = generate_emit_meta_calldata(meta.clone()).unwrap();
265 let calldata2 = generate_emit_meta_calldata(meta.clone()).unwrap();
266 let calldata3 = generate_emit_meta_calldata(meta).unwrap();
267
268 assert_eq!(calldata1, calldata2);
270 assert_eq!(calldata2, calldata3);
271 }
272
273 #[test]
274 fn test_subject_matches_document_hash() {
275 let meta = create_test_meta("content for hash verification");
276 let expected_hash = meta.hash(false).unwrap();
277
278 let calldata = generate_emit_meta_calldata(meta).unwrap();
279 let decoded = emitMetaCall::abi_decode(&calldata).unwrap();
280
281 assert_eq!(decoded.subject, FixedBytes::from(expected_hash));
283 }
284}