use alloy::primitives::{Bytes, FixedBytes, hex};
use alloy::sol_types::SolCall;
use rain_metadata_bindings::MetaBoard::emitMetaCall;
use serde::Serialize;
use crate::{Error, RainMetaDocumentV1Item};
use crate::meta::types::dotrain::source_v1::DotrainSourceV1;
fn generate_emit_data_calldata(subject: FixedBytes<32>, data: Vec<u8>) -> Vec<u8> {
let call = emitMetaCall {
subject,
meta: Bytes::from(data),
};
call.abi_encode()
}
pub fn generate_emit_meta_calldata(meta: RainMetaDocumentV1Item) -> Result<Vec<u8>, Error> {
let meta_bytes = meta.cbor_encode()?;
let hash = meta.hash(false)?;
Ok(generate_emit_data_calldata(hash.into(), meta_bytes))
}
#[derive(Debug, Clone, Serialize, serde::Deserialize)]
pub struct DotrainSourceEmitData {
pub subject: String,
pub meta_bytes: String,
pub calldata: String,
}
fn validate_dotrain_content(content: &str) -> Result<(), Error> {
if content.trim().is_empty() {
return Err(Error::InvalidInput(
"Dotrain content cannot be empty".to_string(),
));
}
Ok(())
}
pub fn generate_dotrain_source_emit_tx_data(content: &str) -> Result<DotrainSourceEmitData, Error> {
validate_dotrain_content(content)?;
let dotrain_source = DotrainSourceV1(content.to_string());
let document: RainMetaDocumentV1Item = dotrain_source.into();
let subject_hash = document.hash(false)?;
let documents = vec![document];
let meta_bytes =
RainMetaDocumentV1Item::cbor_encode_seq(&documents, crate::KnownMagic::RainMetaDocumentV1)?;
let meta_bytes_hex = hex::encode_prefixed(&meta_bytes);
let calldata = generate_emit_data_calldata(subject_hash.into(), meta_bytes);
Ok(DotrainSourceEmitData {
subject: hex::encode_prefixed(subject_hash),
meta_bytes: meta_bytes_hex,
calldata: hex::encode_prefixed(calldata),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::meta::{ContentEncoding, ContentLanguage, ContentType, KnownMagic};
fn create_test_meta(content: &str) -> RainMetaDocumentV1Item {
RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(content),
magic: KnownMagic::DotrainSourceV1,
content_type: ContentType::OctetStream,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
}
}
#[test]
fn test_generate_emit_meta_calldata_success() {
let meta = create_test_meta("test content");
let calldata = generate_emit_meta_calldata(meta.clone()).unwrap();
assert!(!calldata.is_empty());
let decoded = emitMetaCall::abi_decode(&calldata).unwrap();
let expected_hash = meta.hash(false).unwrap();
let expected_meta_bytes = meta.cbor_encode().unwrap();
assert_eq!(decoded.subject, FixedBytes::from(expected_hash));
assert_eq!(decoded.meta.as_ref(), expected_meta_bytes.as_slice());
}
#[test]
fn test_validate_dotrain_content() {
assert!(validate_dotrain_content("#main _ _: int-add(1 2)").is_ok());
assert!(validate_dotrain_content("/* comment */\n#main _ _: 1").is_ok());
assert!(validate_dotrain_content("").is_err());
assert!(validate_dotrain_content(" ").is_err());
assert!(validate_dotrain_content("\n\t\r").is_err());
}
#[test]
fn test_generate_dotrain_source_emit_tx_data_success() {
let content = "#main _ _: int-add(1 2) int-add(2 3)";
let deployment = generate_dotrain_source_emit_tx_data(content).unwrap();
assert!(deployment.subject.starts_with("0x"));
assert!(deployment.meta_bytes.starts_with("0x"));
assert!(deployment.calldata.starts_with("0x"));
assert_eq!(deployment.subject.len(), 66); assert!(deployment.meta_bytes.len() > 10); assert!(deployment.calldata.len() > 10);
let calldata_bytes =
alloy::hex::decode(deployment.calldata.trim_start_matches("0x")).unwrap();
let decoded = emitMetaCall::abi_decode(&calldata_bytes).unwrap();
let subject_hex = deployment.subject.to_lowercase();
assert_eq!(hex::encode_prefixed(decoded.subject), subject_hex);
let meta_bytes_from_field =
alloy::hex::decode(deployment.meta_bytes.trim_start_matches("0x")).unwrap();
assert_eq!(decoded.meta.as_ref(), meta_bytes_from_field.as_slice());
}
#[test]
fn test_generate_dotrain_source_emit_tx_data_empty_content() {
let result = generate_dotrain_source_emit_tx_data("");
assert!(result.is_err());
let result = generate_dotrain_source_emit_tx_data(" ");
assert!(result.is_err());
}
#[test]
fn test_cas_property_same_content() {
let meta1 = create_test_meta("identical content");
let meta2 = create_test_meta("identical content");
let calldata1 = generate_emit_meta_calldata(meta1).unwrap();
let calldata2 = generate_emit_meta_calldata(meta2).unwrap();
assert_eq!(calldata1, calldata2);
}
#[test]
fn test_cas_property_different_content() {
let meta1 = create_test_meta("content A");
let meta2 = create_test_meta("content B");
let calldata1 = generate_emit_meta_calldata(meta1).unwrap();
let calldata2 = generate_emit_meta_calldata(meta2).unwrap();
assert_ne!(calldata1, calldata2);
let decoded1 = emitMetaCall::abi_decode(&calldata1).unwrap();
let decoded2 = emitMetaCall::abi_decode(&calldata2).unwrap();
assert_ne!(decoded1.subject, decoded2.subject);
}
#[test]
fn test_different_magic_same_content() {
let meta1 = create_test_meta("same content");
let mut meta2 = create_test_meta("same content");
meta2.magic = KnownMagic::AuthoringMetaV1;
let calldata1 = generate_emit_meta_calldata(meta1).unwrap();
let calldata2 = generate_emit_meta_calldata(meta2).unwrap();
assert_ne!(calldata1, calldata2);
}
#[test]
fn test_hash_deterministic() {
let meta = create_test_meta("test for deterministic hash");
let calldata1 = generate_emit_meta_calldata(meta.clone()).unwrap();
let calldata2 = generate_emit_meta_calldata(meta.clone()).unwrap();
let calldata3 = generate_emit_meta_calldata(meta).unwrap();
assert_eq!(calldata1, calldata2);
assert_eq!(calldata2, calldata3);
}
#[test]
fn test_subject_matches_document_hash() {
let meta = create_test_meta("content for hash verification");
let expected_hash = meta.hash(false).unwrap();
let calldata = generate_emit_meta_calldata(meta).unwrap();
let decoded = emitMetaCall::abi_decode(&calldata).unwrap();
assert_eq!(decoded.subject, FixedBytes::from(expected_hash));
}
}