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!("inscription content type is not valid UTF-8: {e}"))
78 })?;
79
80 let data = chunks[3].data.as_ref().ok_or_else(|| {
82 ScriptError::InvalidScript("inscription data chunk has no data".to_string())
83 })?;
84
85 Ok(Inscription {
86 content_type,
87 data: data.clone(),
88 })
89 }
90
91 fn make_data_chunk(data: &[u8]) -> ScriptChunk {
93 let len = data.len();
94 let op_byte = if len < 0x4c {
95 len as u8
96 } else if len < 256 {
97 Op::OpPushData1.to_byte()
98 } else if len < 65536 {
99 Op::OpPushData2.to_byte()
100 } else {
101 Op::OpPushData4.to_byte()
102 };
103 ScriptChunk::new_raw(op_byte, Some(data.to_vec()))
104 }
105}
106
107pub fn op_return_data(data: &[u8]) -> LockingScript {
111 let chunks = vec![
112 ScriptChunk::new_opcode(Op::Op0), ScriptChunk::new_opcode(Op::OpReturn),
114 Inscription::make_data_chunk(data),
115 ];
116 LockingScript::from_script(Script::from_chunks(chunks))
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_inscription_to_script_format() {
125 let insc = Inscription::new("text/plain", b"hello world".to_vec());
126 let script = insc.to_script();
127 let binary = script.to_binary();
128
129 assert_eq!(binary[0], 0x00, "should start with OP_FALSE");
131 assert_eq!(binary[1], 0x6a, "second byte should be OP_RETURN");
133 }
134
135 #[test]
136 fn test_inscription_roundtrip() {
137 let insc = Inscription::new("text/plain", b"hello world".to_vec());
138 let script = insc.to_script();
139
140 let decoded = Inscription::from_script(&script).unwrap();
144 assert_eq!(decoded.content_type, "text/plain");
145 assert_eq!(decoded.data, b"hello world");
146 }
147
148 #[test]
149 fn test_inscription_from_script_invalid() {
150 let script = Script::from_chunks(vec![ScriptChunk::new_opcode(Op::Op0)]);
152 assert!(Inscription::from_script(&script).is_err());
153
154 let script = Script::from_chunks(vec![
156 ScriptChunk::new_opcode(Op::Op1),
157 ScriptChunk::new_opcode(Op::OpReturn),
158 ScriptChunk::new_raw(4, Some(b"test".to_vec())),
159 ScriptChunk::new_raw(4, Some(b"data".to_vec())),
160 ]);
161 assert!(Inscription::from_script(&script).is_err());
162 }
163
164 #[test]
165 fn test_op_return_data_format() {
166 let script = op_return_data(b"test data");
167 let binary = script.to_binary();
168
169 assert_eq!(binary[0], 0x00, "OP_FALSE");
170 assert_eq!(binary[1], 0x6a, "OP_RETURN");
171 assert_eq!(binary[2], 9);
173 assert_eq!(&binary[3..12], b"test data");
174 }
175
176 #[test]
177 fn test_op_return_data_empty() {
178 let script = op_return_data(&[]);
179 let binary = script.to_binary();
180
181 assert_eq!(binary[0], 0x00, "OP_FALSE");
182 assert_eq!(binary[1], 0x6a, "OP_RETURN");
183 assert_eq!(binary[2], 0x00, "push 0 bytes");
184 }
185
186 #[test]
187 fn test_inscription_various_content_types() {
188 let test_cases = vec![
189 ("application/json", b"{\"key\":\"value\"}".to_vec()),
190 ("image/png", vec![0x89, 0x50, 0x4e, 0x47]),
191 ("text/html", b"<html></html>".to_vec()),
192 ];
193
194 for (ct, data) in test_cases {
195 let insc = Inscription::new(ct, data.clone());
196 let script = insc.to_script();
197 let decoded = Inscription::from_script(&script).unwrap();
198 assert_eq!(decoded.content_type, ct);
199 assert_eq!(decoded.data, data);
200 }
201 }
202}