bsv_wasm/script/
script_template.rs

1use crate::OpCodes::OP_0;
2use std::str::FromStr;
3
4use crate::{BSVErrors, OpCodes, PublicKey, Script, ScriptBit, Signature, VarInt};
5use hex::FromHexError;
6use num_traits::FromPrimitive;
7use serde::{Deserialize, Serialize};
8use strum_macros::Display;
9use thiserror::Error;
10
11#[cfg(target_arch = "wasm32")]
12use wasm_bindgen::{prelude::*, throw_str, JsValue};
13
14#[derive(Debug, Error)]
15pub enum ScriptTemplateErrors {
16    #[error("Script did not match template at index {0}. {2} is not equal to {1:?}. Error: {3:?}")]
17    MatchFailure(usize, MatchToken, ScriptBit, BSVErrors),
18
19    #[error("Failed to parse OP_DATA code {0}: {1}")]
20    OpDataParse(String, String),
21
22    #[error("Script Template and Script lengths do not match.")]
23    LengthsDiffer,
24
25    #[error("{0}")]
26    MalformedHex(
27        #[from]
28        #[source]
29        FromHexError,
30    ),
31}
32
33#[cfg_attr(all(target_arch = "wasm32"), wasm_bindgen)]
34#[derive(Debug, Clone, Display)]
35pub enum DataLengthConstraints {
36    Equals,
37    GreaterThan,
38    LessThan,
39    GreaterThanOrEquals,
40    LessThanOrEquals,
41}
42
43#[derive(Debug, Clone, Display)]
44pub enum MatchToken {
45    // Precise Matches
46    OpCode(OpCodes),
47    Push(Vec<u8>),
48    PushData(OpCodes, Vec<u8>),
49
50    // Fuzzy matches
51    AnyData,
52    Data(usize, DataLengthConstraints),
53    Signature,
54    PublicKey,
55    PublicKeyHash,
56}
57
58#[cfg_attr(all(target_arch = "wasm32"), wasm_bindgen)]
59#[derive(Debug, Clone, Display, Serialize, Deserialize)]
60pub enum MatchDataTypes {
61    Data,
62    Signature,
63    PublicKey,
64    PublicKeyHash,
65}
66
67#[cfg_attr(all(target_arch = "wasm32"), wasm_bindgen)]
68#[derive(Debug, Clone)]
69pub struct ScriptTemplate(Vec<MatchToken>);
70
71impl ScriptTemplate {
72    fn map_string_to_match_token(code: &str) -> Result<MatchToken, ScriptTemplateErrors> {
73        // Number OP_CODES
74        if let Ok(num_code) = u8::from_str(code) {
75            match num_code {
76                0 => return Ok(MatchToken::OpCode(OP_0)),
77                v @ 1..=16 => return Ok(MatchToken::OpCode(OpCodes::from_u8(v + 80).unwrap())),
78                _ => (),
79            }
80        }
81
82        // Standard OP_CODES
83        match OpCodes::from_str(code) {
84            Ok(OpCodes::OP_SIG) => return Ok(MatchToken::Signature),
85            Ok(OpCodes::OP_PUBKEY) => return Ok(MatchToken::PublicKey),
86            Ok(OpCodes::OP_PUBKEYHASH) => return Ok(MatchToken::PublicKeyHash),
87            Ok(OpCodes::OP_DATA) => return Ok(MatchToken::AnyData),
88
89            Ok(v) => return Ok(MatchToken::OpCode(v)),
90            Err(_) => (),
91        }
92
93        if code.starts_with(&OpCodes::OP_DATA.to_string()) {
94            // Match on >=
95            if let Some((_, length_str)) = code.split_once(">=") {
96                let len = usize::from_str(length_str).map_err(|e| ScriptTemplateErrors::OpDataParse(code.to_string(), e.to_string()))?;
97                return Ok(MatchToken::Data(len, DataLengthConstraints::GreaterThanOrEquals));
98            }
99
100            // Match on <=
101            if let Some((_, length_str)) = code.split_once("<=") {
102                let len = usize::from_str(length_str).map_err(|e| ScriptTemplateErrors::OpDataParse(code.to_string(), e.to_string()))?;
103                return Ok(MatchToken::Data(len, DataLengthConstraints::LessThanOrEquals));
104            }
105
106            // Match on =
107            if let Some((_, length_str)) = code.split_once('=') {
108                let len = usize::from_str(length_str).map_err(|e| ScriptTemplateErrors::OpDataParse(code.to_string(), e.to_string()))?;
109                return Ok(MatchToken::Data(len, DataLengthConstraints::Equals));
110            }
111
112            // Match on >
113            if let Some((_, length_str)) = code.split_once('>') {
114                let len = usize::from_str(length_str).map_err(|e| ScriptTemplateErrors::OpDataParse(code.to_string(), e.to_string()))?;
115                return Ok(MatchToken::Data(len, DataLengthConstraints::GreaterThan));
116            }
117
118            // Match on <
119            if let Some((_, length_str)) = code.split_once('<') {
120                let len = usize::from_str(length_str).map_err(|e| ScriptTemplateErrors::OpDataParse(code.to_string(), e.to_string()))?;
121                return Ok(MatchToken::Data(len, DataLengthConstraints::LessThan));
122            }
123        }
124
125        // PUSHDATA OP_CODES
126        let data_bytes = hex::decode(code)?;
127        let token = match VarInt::get_pushdata_opcode(data_bytes.len() as u64) {
128            Some(v) => MatchToken::PushData(v, data_bytes),
129            None => MatchToken::Push(data_bytes),
130        };
131
132        Ok(token)
133    }
134
135    pub fn from_script_impl(script: &Script) -> Result<ScriptTemplate, ScriptTemplateErrors> {
136        ScriptTemplate::from_asm_string_impl(&script.to_asm_string_impl(false))
137    }
138
139    pub fn from_asm_string_impl(asm: &str) -> Result<ScriptTemplate, ScriptTemplateErrors> {
140        let tokens: Result<Vec<_>, _> = asm.split(' ').map(ScriptTemplate::map_string_to_match_token).collect();
141
142        Ok(ScriptTemplate(tokens?))
143    }
144}
145
146#[cfg(not(feature = "wasm-bindgen-script-template"))]
147impl ScriptTemplate {
148    pub fn from_script(script: &Script) -> Result<ScriptTemplate, ScriptTemplateErrors> {
149        ScriptTemplate::from_script_impl(script)
150    }
151
152    pub fn from_asm_string(asm: &str) -> Result<ScriptTemplate, ScriptTemplateErrors> {
153        ScriptTemplate::from_asm_string_impl(asm)
154    }
155}
156
157#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen-script-template"))]
158#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-script-template"), wasm_bindgen)]
159impl ScriptTemplate {
160    pub fn from_script(script: &Script) -> Result<ScriptTemplate, JsValue> {
161        match ScriptTemplate::from_script_impl(script) {
162            Ok(v) => Ok(v),
163            Err(e) => Err(JsValue::from_str(&e.to_string())),
164        }
165    }
166
167    pub fn from_asm_string(asm: &str) -> Result<ScriptTemplate, JsValue> {
168        match ScriptTemplate::from_asm_string_impl(asm) {
169            Ok(v) => Ok(v),
170            Err(e) => Err(JsValue::from_str(&e.to_string())),
171        }
172    }
173}
174
175/**
176 * Script Template
177 */
178impl Script {
179    pub fn match_impl(&self, script_template: &ScriptTemplate) -> Result<Vec<(MatchDataTypes, Vec<u8>)>, ScriptTemplateErrors> {
180        if self.0.len() != script_template.0.len() {
181            return Err(ScriptTemplateErrors::LengthsDiffer);
182        }
183
184        let mut matches = vec![];
185
186        for (i, (template, script)) in script_template.0.iter().zip(self.0.iter()).enumerate() {
187            let is_match = match (template, script) {
188                (MatchToken::OpCode(tmpl_code), ScriptBit::OpCode(op_code)) => Ok(tmpl_code == op_code),
189                (MatchToken::Push(tmpl_data), ScriptBit::Push(data)) => Ok(tmpl_data == data),
190                (MatchToken::PushData(tmpl_op, tmpl_data), ScriptBit::PushData(op, data)) => Ok(tmpl_op == op && tmpl_data == data),
191
192                (MatchToken::Data(len, constraint), ScriptBit::PushData(_, data) | ScriptBit::Push(data)) => match constraint {
193                    DataLengthConstraints::Equals => Ok(&data.len() == len),
194                    DataLengthConstraints::GreaterThan => Ok(&data.len() > len),
195                    DataLengthConstraints::LessThan => Ok(&data.len() < len),
196                    DataLengthConstraints::GreaterThanOrEquals => Ok(&data.len() >= len),
197                    DataLengthConstraints::LessThanOrEquals => Ok(&data.len() <= len),
198                },
199
200                (MatchToken::AnyData, ScriptBit::Push(_)) => Ok(true),
201                (MatchToken::AnyData, ScriptBit::PushData(_, _)) => Ok(true),
202
203                (MatchToken::Signature, ScriptBit::Push(sig_buf)) => Signature::from_der_impl(sig_buf).map(|_| true),
204
205                (MatchToken::PublicKey, ScriptBit::Push(pubkey_buf)) => PublicKey::from_bytes_impl(pubkey_buf).map(|_| true),
206
207                (MatchToken::PublicKeyHash, ScriptBit::Push(pubkeyhash_buf)) => Ok(pubkeyhash_buf.len() == 20), // OP_HASH160
208
209                _ => Ok(false),
210            };
211
212            match is_match {
213                Ok(_) => (),
214                Err(e) => {
215                    return Err(ScriptTemplateErrors::MatchFailure(i, template.clone(), script.clone(), e));
216                }
217            }
218
219            // Now that we know script bit is a match, we can add the data parts to the matches array.
220            match (template, script) {
221                (MatchToken::Data(_, _), ScriptBit::PushData(_, data) | ScriptBit::Push(data)) => matches.push((MatchDataTypes::Data, data.clone())),
222
223                (MatchToken::AnyData, ScriptBit::Push(data)) => matches.push((MatchDataTypes::Data, data.clone())),
224                (MatchToken::AnyData, ScriptBit::PushData(_, data)) => matches.push((MatchDataTypes::Data, data.clone())),
225
226                (MatchToken::Signature, ScriptBit::Push(data)) => matches.push((MatchDataTypes::Signature, data.clone())),
227
228                (MatchToken::PublicKey, ScriptBit::Push(data)) => matches.push((MatchDataTypes::PublicKey, data.clone())),
229
230                (MatchToken::PublicKeyHash, ScriptBit::Push(data)) => matches.push((MatchDataTypes::PublicKeyHash, data.clone())), // OP_HASH160
231                _ => (),
232            }
233        }
234
235        Ok(matches)
236    }
237
238    pub fn test_impl(&self, script_template: &ScriptTemplate) -> bool {
239        self.match_impl(script_template).is_ok()
240    }
241}
242
243#[cfg(not(feature = "wasm-bindgen-script-template"))]
244impl Script {
245    /// Matches the Script against the provided ScriptTemplate.
246    ///
247    /// If any data can be gleaned from the Script (ie. OP_DATA, OP_PUBKEY, OP_SIG, etc.), it will return it in a `Vec<Match>`
248    ///
249    /// # Example
250    /// ```
251    /// use bsv_wasm::{ Script, MatchDataTypes, ScriptTemplate };
252    ///
253    /// let script = Script::from_asm_string("OP_HASH160 b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc6 OP_EQUALVERIFY OP_DUP OP_HASH160 f9dfc5a4ae5256e5938c2d819738f7b57e4d7b46 OP_EQUALVERIFY OP_CHECKSIG OP_RETURN 21e8").unwrap();
254    /// let script_template = ScriptTemplate::from_asm_string("OP_HASH160 OP_DATA=20 OP_EQUALVERIFY OP_DUP OP_HASH160 OP_PUBKEYHASH OP_EQUALVERIFY OP_CHECKSIG OP_RETURN OP_DATA").unwrap();
255    ///
256    /// let match_result = script.matches(&script_template);
257    /// let extracted = match_result.unwrap();
258    /// assert_eq!(extracted.len(), 3);
259    /// match &extracted[0] {
260    ///    (MatchDataTypes::Data, v) => {
261    ///        assert_eq!(v.len(), 20, "Data was not 20 bytes long");
262    ///        assert_eq!(v, &hex::decode("b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc6").unwrap())
263    ///    }
264    ///    _ => assert!(false, "Index 0 did not contain Signature"),
265    /// }
266    /// ```
267    pub fn matches(&self, script_template: &ScriptTemplate) -> Result<Vec<(MatchDataTypes, Vec<u8>)>, ScriptTemplateErrors> {
268        self.match_impl(script_template)
269    }
270
271    /// Matches the Script against the provided ScriptTemplate.
272    ///
273    /// Returns `true` if the Script matches the ScriptTemplate.
274    pub fn is_match(&self, script_template: &ScriptTemplate) -> bool {
275        self.test_impl(script_template)
276    }
277}
278
279#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen-script-template"))]
280#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-script-template"), wasm_bindgen)]
281impl Script {
282    /// Matches the Script against the provided ScriptTemplate.
283    ///
284    /// If any data can be gleaned from the Script (ie. OP_DATA, OP_PUBKEY, OP_SIG, etc.), it will return it in a `Vec<Match>`
285    /// @returns {[string, Uint8Array][]}
286    pub fn matches(&self, script_template: &ScriptTemplate) -> Result<JsValue, JsValue> {
287        let matches = match self.match_impl(script_template) {
288            Ok(v) => v,
289            Err(e) => return Err(JsValue::from_str(&e.to_string())),
290        };
291
292        match JsValue::from_serde(&matches) {
293            Ok(v) => Ok(v),
294            Err(e) => Err(JsValue::from_str(&e.to_string())),
295        }
296    }
297
298    /// Matches the Script against the provided ScriptTemplate.
299    ///
300    /// Returns `true` if the Script matches the ScriptTemplate.
301    /// #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = isMatch))]
302    pub fn is_match(&self, script_template: &ScriptTemplate) -> bool {
303        self.test_impl(script_template)
304    }
305}