bsv/script/
inscriptions.rs1use crate::script::error::ScriptError;
8use crate::script::locking_script::LockingScript;
9use crate::script::op::Op;
10use crate::script::script::Script;
11use crate::script::script_chunk::ScriptChunk;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Inscription {
19 pub content_type: String,
20 pub data: Vec<u8>,
21}
22
23impl Inscription {
24 pub fn new(content_type: &str, data: Vec<u8>) -> Self {
26 Inscription {
27 content_type: content_type.to_string(),
28 data,
29 }
30 }
31
32 pub fn to_script(&self) -> LockingScript {
36 let ct_bytes = self.content_type.as_bytes().to_vec();
37 let chunks = vec![
38 ScriptChunk::new_opcode(Op::Op0), ScriptChunk::new_opcode(Op::OpReturn),
40 Self::make_data_chunk(&ct_bytes),
41 Self::make_data_chunk(&self.data),
42 ];
43 LockingScript::from_script(Script::from_chunks(chunks))
44 }
45
46 pub fn from_script(script: &Script) -> Result<Self, ScriptError> {
50 let chunks = script.chunks();
51
52 if chunks.len() < 4 {
53 return Err(ScriptError::InvalidScript(
54 "inscription script must have at least 4 chunks".to_string(),
55 ));
56 }
57
58 if chunks[0].op != Op::Op0 {
60 return Err(ScriptError::InvalidScript(
61 "inscription must start with OP_FALSE/OP_0".to_string(),
62 ));
63 }
64
65 if chunks[1].op != Op::OpReturn {
67 return Err(ScriptError::InvalidScript(
68 "inscription second opcode must be OP_RETURN".to_string(),
69 ));
70 }
71
72 let ct_data = chunks[2].data.as_ref().ok_or_else(|| {
74 ScriptError::InvalidScript("inscription content type chunk has no data".to_string())
75 })?;
76 let content_type = String::from_utf8(ct_data.clone()).map_err(|e| {
77 ScriptError::InvalidScript(format!(
78 "inscription content type is not valid UTF-8: {}",
79 e
80 ))
81 })?;
82
83 let data = chunks[3].data.as_ref().ok_or_else(|| {
85 ScriptError::InvalidScript("inscription data chunk has no data".to_string())
86 })?;
87
88 Ok(Inscription {
89 content_type,
90 data: data.clone(),
91 })
92 }
93
94 fn make_data_chunk(data: &[u8]) -> ScriptChunk {
96 let len = data.len();
97 let op_byte = if len < 0x4c {
98 len as u8
99 } else if len < 256 {
100 Op::OpPushData1.to_byte()
101 } else if len < 65536 {
102 Op::OpPushData2.to_byte()
103 } else {
104 Op::OpPushData4.to_byte()
105 };
106 ScriptChunk::new_raw(op_byte, Some(data.to_vec()))
107 }
108}
109
110pub fn op_return_data(data: &[u8]) -> LockingScript {
114 let chunks = vec![
115 ScriptChunk::new_opcode(Op::Op0), ScriptChunk::new_opcode(Op::OpReturn),
117 Inscription::make_data_chunk(data),
118 ];
119 LockingScript::from_script(Script::from_chunks(chunks))
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_inscription_to_script_format() {
128 let insc = Inscription::new("text/plain", b"hello world".to_vec());
129 let script = insc.to_script();
130 let binary = script.to_binary();
131
132 assert_eq!(binary[0], 0x00, "should start with OP_FALSE");
134 assert_eq!(binary[1], 0x6a, "second byte should be OP_RETURN");
136 }
137
138 #[test]
139 fn test_inscription_roundtrip() {
140 let insc = Inscription::new("text/plain", b"hello world".to_vec());
141 let script = insc.to_script();
142
143 let decoded = Inscription::from_script(&script).unwrap();
147 assert_eq!(decoded.content_type, "text/plain");
148 assert_eq!(decoded.data, b"hello world");
149 }
150
151 #[test]
152 fn test_inscription_from_script_invalid() {
153 let script = Script::from_chunks(vec![ScriptChunk::new_opcode(Op::Op0)]);
155 assert!(Inscription::from_script(&script).is_err());
156
157 let script = Script::from_chunks(vec![
159 ScriptChunk::new_opcode(Op::Op1),
160 ScriptChunk::new_opcode(Op::OpReturn),
161 ScriptChunk::new_raw(4, Some(b"test".to_vec())),
162 ScriptChunk::new_raw(4, Some(b"data".to_vec())),
163 ]);
164 assert!(Inscription::from_script(&script).is_err());
165 }
166
167 #[test]
168 fn test_op_return_data_format() {
169 let script = op_return_data(b"test data");
170 let binary = script.to_binary();
171
172 assert_eq!(binary[0], 0x00, "OP_FALSE");
173 assert_eq!(binary[1], 0x6a, "OP_RETURN");
174 assert_eq!(binary[2], 9);
176 assert_eq!(&binary[3..12], b"test data");
177 }
178
179 #[test]
180 fn test_op_return_data_empty() {
181 let script = op_return_data(&[]);
182 let binary = script.to_binary();
183
184 assert_eq!(binary[0], 0x00, "OP_FALSE");
185 assert_eq!(binary[1], 0x6a, "OP_RETURN");
186 assert_eq!(binary[2], 0x00, "push 0 bytes");
187 }
188
189 #[test]
190 fn test_inscription_various_content_types() {
191 let test_cases = vec![
192 ("application/json", b"{\"key\":\"value\"}".to_vec()),
193 ("image/png", vec![0x89, 0x50, 0x4e, 0x47]),
194 ("text/html", b"<html></html>".to_vec()),
195 ];
196
197 for (ct, data) in test_cases {
198 let insc = Inscription::new(ct, data.clone());
199 let script = insc.to_script();
200 let decoded = Inscription::from_script(&script).unwrap();
201 assert_eq!(decoded.content_type, ct);
202 assert_eq!(decoded.data, data);
203 }
204 }
205}