Skip to main content

rain_metadata/
metaboard.rs

1use 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
9/// Generate calldata for IMetaBoardV1_2.emitMeta() function using raw bytes
10fn 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
18/// Generate calldata for IMetaBoardV1_2.emitMeta() function from any RainMetaDocumentV1Item
19pub 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/// Deployment data for publishing to MetaBoard
26#[derive(Debug, Clone, Serialize, serde::Deserialize)]
27pub struct DotrainSourceEmitData {
28    /// Keccak256 hash of the content (hex-encoded with 0x prefix)
29    pub subject: String,
30    /// CBOR-encoded metadata bytes (hex-encoded with 0x prefix)
31    pub meta_bytes: String,
32    /// Complete calldata for IMetaBoardV1_2.emitMeta() (hex-encoded with 0x prefix)
33    pub calldata: String,
34}
35
36/// Validate dotrain content - currently only checks for non-empty content
37fn validate_dotrain_content(content: &str) -> Result<(), Error> {
38    // Reject empty or whitespace-only content
39    if content.trim().is_empty() {
40        return Err(Error::InvalidInput(
41            "Dotrain content cannot be empty".to_string(),
42        ));
43    }
44    Ok(())
45}
46
47/// Generate tx data for DotrainSourceV1 content
48/// Validates content and returns subject hash, meta bytes, and calldata
49pub fn generate_dotrain_source_emit_tx_data(content: &str) -> Result<DotrainSourceEmitData, Error> {
50    // Validate content
51    validate_dotrain_content(content)?;
52
53    // Create DotrainSourceV1
54    let dotrain_source = DotrainSourceV1(content.to_string());
55    // Convert to RainMetaDocumentV1Item
56    let document: RainMetaDocumentV1Item = dotrain_source.into();
57
58    // Hash before moving the document into the sequence to avoid cloning.
59    // Calculate subject hash
60    let subject_hash = document.hash(false)?;
61    let documents = vec![document];
62
63    // Generate CBOR bytes
64    let meta_bytes =
65        RainMetaDocumentV1Item::cbor_encode_seq(&documents, crate::KnownMagic::RainMetaDocumentV1)?;
66
67    // Prepare hex-encoded meta first to avoid cloning later
68    let meta_bytes_hex = hex::encode_prefixed(&meta_bytes);
69    // Generate calldata
70    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        // Verify calldata is not empty
101        assert!(!calldata.is_empty());
102
103        // Decode and verify structure
104        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        // Valid content
115        assert!(validate_dotrain_content("#main _ _: int-add(1 2)").is_ok());
116        assert!(validate_dotrain_content("/* comment */\n#main _ _: 1").is_ok());
117
118        // Invalid content
119        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        // Check all fields have 0x prefix
130        assert!(deployment.subject.starts_with("0x"));
131        assert!(deployment.meta_bytes.starts_with("0x"));
132        assert!(deployment.calldata.starts_with("0x"));
133
134        // Check lengths are reasonable
135        assert_eq!(deployment.subject.len(), 66); // 0x + 64 hex chars
136        assert!(deployment.meta_bytes.len() > 10); // Should have some content
137        assert!(deployment.calldata.len() > 10); // Should have some content
138
139        // Optional: verify subject in calldata matches the declared subject
140        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        // meta in calldata must equal the meta_bytes field
147        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
152    #[test]
153    fn test_generate_dotrain_source_emit_tx_data_empty_content() {
154        let result = generate_dotrain_source_emit_tx_data("");
155        assert!(result.is_err());
156
157        let result = generate_dotrain_source_emit_tx_data("   ");
158        assert!(result.is_err());
159    }
160
161    #[test]
162    fn test_cas_property_same_content() {
163        // Content Addressable Storage: same content -> same calldata
164        let meta1 = create_test_meta("identical content");
165        let meta2 = create_test_meta("identical content");
166
167        let calldata1 = generate_emit_meta_calldata(meta1).unwrap();
168        let calldata2 = generate_emit_meta_calldata(meta2).unwrap();
169
170        // Same content should produce identical calldata
171        assert_eq!(calldata1, calldata2);
172    }
173
174    #[test]
175    fn test_cas_property_different_content() {
176        let meta1 = create_test_meta("content A");
177        let meta2 = create_test_meta("content B");
178
179        let calldata1 = generate_emit_meta_calldata(meta1).unwrap();
180        let calldata2 = generate_emit_meta_calldata(meta2).unwrap();
181
182        // Different content should produce different calldata
183        assert_ne!(calldata1, calldata2);
184
185        // Verify different subjects
186        let decoded1 = emitMetaCall::abi_decode(&calldata1).unwrap();
187        let decoded2 = emitMetaCall::abi_decode(&calldata2).unwrap();
188        assert_ne!(decoded1.subject, decoded2.subject);
189    }
190
191    #[test]
192    fn test_different_magic_same_content() {
193        let meta1 = create_test_meta("same content");
194        let mut meta2 = create_test_meta("same content");
195        meta2.magic = KnownMagic::AuthoringMetaV1; // Different magic
196
197        let calldata1 = generate_emit_meta_calldata(meta1).unwrap();
198        let calldata2 = generate_emit_meta_calldata(meta2).unwrap();
199
200        // Different magic should produce different calldata (even with same payload)
201        assert_ne!(calldata1, calldata2);
202    }
203
204    #[test]
205    fn test_hash_deterministic() {
206        let meta = create_test_meta("test for deterministic hash");
207
208        // Generate calldata multiple times
209        let calldata1 = generate_emit_meta_calldata(meta.clone()).unwrap();
210        let calldata2 = generate_emit_meta_calldata(meta.clone()).unwrap();
211        let calldata3 = generate_emit_meta_calldata(meta).unwrap();
212
213        // All should be identical (deterministic hashing)
214        assert_eq!(calldata1, calldata2);
215        assert_eq!(calldata2, calldata3);
216    }
217
218    #[test]
219    fn test_subject_matches_document_hash() {
220        let meta = create_test_meta("content for hash verification");
221        let expected_hash = meta.hash(false).unwrap();
222
223        let calldata = generate_emit_meta_calldata(meta).unwrap();
224        let decoded = emitMetaCall::abi_decode(&calldata).unwrap();
225
226        // Subject in calldata should match document's own hash
227        assert_eq!(decoded.subject, FixedBytes::from(expected_hash));
228    }
229}