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> {
20 let meta_bytes = meta.cbor_encode()?;
21 let hash = meta.hash(false)?;
22 Ok(generate_emit_data_calldata(hash.into(), meta_bytes))
23}
24
25#[derive(Debug, Clone, Serialize, serde::Deserialize)]
27pub struct DotrainSourceEmitData {
28 pub subject: String,
30 pub meta_bytes: String,
32 pub calldata: String,
34}
35
36fn validate_dotrain_content(content: &str) -> Result<(), Error> {
38 if content.trim().is_empty() {
40 return Err(Error::InvalidInput(
41 "Dotrain content cannot be empty".to_string(),
42 ));
43 }
44 Ok(())
45}
46
47pub fn generate_dotrain_source_emit_tx_data(content: &str) -> Result<DotrainSourceEmitData, Error> {
50 validate_dotrain_content(content)?;
52
53 let dotrain_source = DotrainSourceV1(content.to_string());
55 let document: RainMetaDocumentV1Item = dotrain_source.into();
57
58 let subject_hash = document.hash(false)?;
61 let documents = vec![document];
62
63 let meta_bytes =
65 RainMetaDocumentV1Item::cbor_encode_seq(&documents, crate::KnownMagic::RainMetaDocumentV1)?;
66
67 let meta_bytes_hex = hex::encode_prefixed(&meta_bytes);
69 let calldata = generate_emit_data_calldata(subject_hash.into(), meta_bytes);
71
72 Ok(DotrainSourceEmitData {
73 subject: hex::encode_prefixed(subject_hash),
74 meta_bytes: meta_bytes_hex,
75 calldata: hex::encode_prefixed(calldata),
76 })
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::meta::{ContentEncoding, ContentLanguage, ContentType, KnownMagic};
83
84 fn create_test_meta(content: &str) -> RainMetaDocumentV1Item {
85 RainMetaDocumentV1Item {
86 payload: serde_bytes::ByteBuf::from(content),
87 magic: KnownMagic::DotrainSourceV1,
88 content_type: ContentType::OctetStream,
89 content_encoding: ContentEncoding::None,
90 content_language: ContentLanguage::None,
91 schema: None,
92 }
93 }
94
95 #[test]
96 fn test_generate_emit_meta_calldata_success() {
97 let meta = create_test_meta("test content");
98 let calldata = generate_emit_meta_calldata(meta.clone()).unwrap();
99
100 assert!(!calldata.is_empty());
102
103 let decoded = emitMetaCall::abi_decode(&calldata).unwrap();
105 let expected_hash = meta.hash(false).unwrap();
106 let expected_meta_bytes = meta.cbor_encode().unwrap();
107
108 assert_eq!(decoded.subject, FixedBytes::from(expected_hash));
109 assert_eq!(decoded.meta.as_ref(), expected_meta_bytes.as_slice());
110 }
111
112 #[test]
113 fn test_validate_dotrain_content() {
114 assert!(validate_dotrain_content("#main _ _: int-add(1 2)").is_ok());
116 assert!(validate_dotrain_content("/* comment */\n#main _ _: 1").is_ok());
117
118 assert!(validate_dotrain_content("").is_err());
120 assert!(validate_dotrain_content(" ").is_err());
121 assert!(validate_dotrain_content("\n\t\r").is_err());
122 }
123
124 #[test]
125 fn test_generate_dotrain_source_emit_tx_data_success() {
126 let content = "#main _ _: int-add(1 2) int-add(2 3)";
127 let deployment = generate_dotrain_source_emit_tx_data(content).unwrap();
128
129 assert!(deployment.subject.starts_with("0x"));
131 assert!(deployment.meta_bytes.starts_with("0x"));
132 assert!(deployment.calldata.starts_with("0x"));
133
134 assert_eq!(deployment.subject.len(), 66); assert!(deployment.meta_bytes.len() > 10); assert!(deployment.calldata.len() > 10); let calldata_bytes =
141 alloy::hex::decode(deployment.calldata.trim_start_matches("0x")).unwrap();
142 let decoded = emitMetaCall::abi_decode(&calldata_bytes).unwrap();
143 let subject_hex = deployment.subject.to_lowercase();
144 assert_eq!(hex::encode_prefixed(decoded.subject), subject_hex);
145
146 let meta_bytes_from_field =
148 alloy::hex::decode(deployment.meta_bytes.trim_start_matches("0x")).unwrap();
149 assert_eq!(decoded.meta.as_ref(), meta_bytes_from_field.as_slice());
150
151 assert!(deployment.meta_bytes.starts_with("0xff0a89c674ee7874"));
154
155 let item = RainMetaDocumentV1Item {
159 payload: serde_bytes::ByteBuf::from(content.as_bytes().to_vec()),
160 magic: KnownMagic::DotrainSourceV1,
161 content_type: ContentType::OctetStream,
162 content_encoding: ContentEncoding::None,
163 content_language: ContentLanguage::None,
164 schema: None,
165 };
166 let expected_subject = alloy::primitives::keccak256(item.cbor_encode().unwrap());
167 assert_eq!(deployment.subject, hex::encode_prefixed(expected_subject));
168 }
169
170 #[test]
171 fn test_generate_dotrain_source_emit_tx_data_empty_content() {
172 let result = generate_dotrain_source_emit_tx_data("");
173 assert!(result.is_err());
174
175 let result = generate_dotrain_source_emit_tx_data(" ");
176 assert!(result.is_err());
177 }
178
179 #[test]
180 fn test_cas_property_same_content() {
181 let meta1 = create_test_meta("identical content");
183 let meta2 = create_test_meta("identical content");
184
185 let calldata1 = generate_emit_meta_calldata(meta1).unwrap();
186 let calldata2 = generate_emit_meta_calldata(meta2).unwrap();
187
188 assert_eq!(calldata1, calldata2);
190 }
191
192 #[test]
193 fn test_cas_property_different_content() {
194 let meta1 = create_test_meta("content A");
195 let meta2 = create_test_meta("content B");
196
197 let calldata1 = generate_emit_meta_calldata(meta1).unwrap();
198 let calldata2 = generate_emit_meta_calldata(meta2).unwrap();
199
200 assert_ne!(calldata1, calldata2);
202
203 let decoded1 = emitMetaCall::abi_decode(&calldata1).unwrap();
205 let decoded2 = emitMetaCall::abi_decode(&calldata2).unwrap();
206 assert_ne!(decoded1.subject, decoded2.subject);
207 }
208
209 #[test]
210 fn test_different_magic_same_content() {
211 let meta1 = create_test_meta("same content");
212 let mut meta2 = create_test_meta("same content");
213 meta2.magic = KnownMagic::AuthoringMetaV1; let calldata1 = generate_emit_meta_calldata(meta1).unwrap();
216 let calldata2 = generate_emit_meta_calldata(meta2).unwrap();
217
218 assert_ne!(calldata1, calldata2);
220 }
221
222 #[test]
223 fn test_hash_deterministic() {
224 let meta = create_test_meta("test for deterministic hash");
225
226 let calldata1 = generate_emit_meta_calldata(meta.clone()).unwrap();
228 let calldata2 = generate_emit_meta_calldata(meta.clone()).unwrap();
229 let calldata3 = generate_emit_meta_calldata(meta).unwrap();
230
231 assert_eq!(calldata1, calldata2);
233 assert_eq!(calldata2, calldata3);
234 }
235
236 #[test]
237 fn test_subject_matches_document_hash() {
238 let meta = create_test_meta("content for hash verification");
239 let expected_hash = meta.hash(false).unwrap();
240
241 let calldata = generate_emit_meta_calldata(meta).unwrap();
242 let decoded = emitMetaCall::abi_decode(&calldata).unwrap();
243
244 assert_eq!(decoded.subject, FixedBytes::from(expected_hash));
246 }
247}