bsv_rs/script/template.rs
1//! Script template trait and types.
2//!
3//! This module provides the [`ScriptTemplate`] trait for creating reusable script patterns,
4//! and the [`ScriptTemplateUnlock`] type for generating unlocking scripts.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use bsv_rs::script::templates::P2PKH;
10//! use bsv_rs::script::template::ScriptTemplate;
11//!
12//! let template = P2PKH::new();
13//! let locking_script = template.lock(&pubkey_hash)?;
14//! ```
15
16use crate::primitives::bsv::sighash::{
17 compute_sighash_for_signing, parse_transaction, SighashParams, SIGHASH_ALL,
18 SIGHASH_ANYONECANPAY, SIGHASH_FORKID, SIGHASH_NONE, SIGHASH_SINGLE,
19};
20use crate::primitives::bsv::TransactionSignature;
21use crate::primitives::ec::PrivateKey;
22use crate::script::{LockingScript, UnlockingScript};
23use crate::Result;
24
25// Re-export for convenience
26pub use crate::script::Script;
27
28/// Specifies which outputs to sign in a transaction signature.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum SignOutputs {
31 /// Sign all outputs (SIGHASH_ALL).
32 All,
33 /// Sign no outputs (SIGHASH_NONE).
34 None,
35 /// Sign only the output at the same index as the input (SIGHASH_SINGLE).
36 Single,
37}
38
39impl SignOutputs {
40 /// Converts to the sighash flag value.
41 pub fn to_sighash_flag(self) -> u32 {
42 match self {
43 SignOutputs::All => SIGHASH_ALL,
44 SignOutputs::None => SIGHASH_NONE,
45 SignOutputs::Single => SIGHASH_SINGLE,
46 }
47 }
48}
49
50/// Context for signing a transaction input.
51///
52/// This provides all the information needed to compute a sighash and produce
53/// a signature for a specific input.
54#[derive(Debug, Clone)]
55pub struct SigningContext<'a> {
56 /// The raw transaction bytes being signed.
57 pub raw_tx: &'a [u8],
58 /// The index of the input being signed.
59 pub input_index: usize,
60 /// The satoshi value of the UTXO being spent.
61 pub source_satoshis: u64,
62 /// The locking script of the UTXO being spent.
63 pub locking_script: &'a Script,
64}
65
66impl<'a> SigningContext<'a> {
67 /// Creates a new signing context.
68 pub fn new(
69 raw_tx: &'a [u8],
70 input_index: usize,
71 source_satoshis: u64,
72 locking_script: &'a Script,
73 ) -> Self {
74 Self {
75 raw_tx,
76 input_index,
77 source_satoshis,
78 locking_script,
79 }
80 }
81
82 /// Computes the sighash for this input with the given scope.
83 ///
84 /// The scope should include SIGHASH_FORKID for BSV transactions.
85 pub fn compute_sighash(&self, scope: u32) -> Result<[u8; 32]> {
86 let tx = parse_transaction(self.raw_tx)?;
87 let subscript = self.locking_script.to_binary();
88
89 Ok(compute_sighash_for_signing(&SighashParams {
90 version: tx.version,
91 inputs: &tx.inputs,
92 outputs: &tx.outputs,
93 locktime: tx.locktime,
94 input_index: self.input_index,
95 subscript: &subscript,
96 satoshis: self.source_satoshis,
97 scope,
98 }))
99 }
100}
101
102/// Computes the sighash scope byte from options.
103///
104/// # Arguments
105///
106/// * `sign_outputs` - Which outputs to sign
107/// * `anyone_can_pay` - Whether to allow other inputs to be added
108///
109/// # Returns
110///
111/// The sighash scope value (always includes SIGHASH_FORKID for BSV)
112pub fn compute_sighash_scope(sign_outputs: SignOutputs, anyone_can_pay: bool) -> u32 {
113 let mut scope = SIGHASH_FORKID | sign_outputs.to_sighash_flag();
114
115 if anyone_can_pay {
116 scope |= SIGHASH_ANYONECANPAY;
117 }
118
119 scope
120}
121
122/// A trait for reusable script patterns.
123///
124/// Script templates provide a high-level API for creating common script types
125/// like P2PKH (Pay-to-Public-Key-Hash) and RPuzzle scripts.
126///
127/// # Example
128///
129/// ```rust,ignore
130/// use bsv_rs::script::templates::P2PKH;
131/// use bsv_rs::script::template::ScriptTemplate;
132///
133/// let template = P2PKH::new();
134/// let locking_script = template.lock(&pubkey_hash)?;
135/// ```
136pub trait ScriptTemplate {
137 /// Creates a locking script with the given parameters.
138 ///
139 /// # Arguments
140 ///
141 /// * `params` - The parameters required to create the locking script.
142 /// For P2PKH, this is the 20-byte public key hash.
143 /// For RPuzzle, this is the R value or its hash.
144 ///
145 /// # Returns
146 ///
147 /// The locking script, or an error if parameters are invalid.
148 fn lock(&self, params: &[u8]) -> Result<LockingScript>;
149}
150
151/// Return type for template unlock methods.
152///
153/// Contains functions to sign a transaction input and estimate the
154/// unlocking script length.
155#[allow(clippy::type_complexity)]
156pub struct ScriptTemplateUnlock {
157 sign_fn: Box<dyn Fn(&SigningContext) -> Result<UnlockingScript> + Send + Sync>,
158 estimate_length_fn: Box<dyn Fn() -> usize + Send + Sync>,
159}
160
161impl ScriptTemplateUnlock {
162 /// Creates a new ScriptTemplateUnlock.
163 ///
164 /// # Arguments
165 ///
166 /// * `sign_fn` - A function that signs a transaction input
167 /// * `estimate_length_fn` - A function that estimates the unlocking script length
168 pub fn new<S, E>(sign_fn: S, estimate_length_fn: E) -> Self
169 where
170 S: Fn(&SigningContext) -> Result<UnlockingScript> + Send + Sync + 'static,
171 E: Fn() -> usize + Send + Sync + 'static,
172 {
173 Self {
174 sign_fn: Box::new(sign_fn),
175 estimate_length_fn: Box::new(estimate_length_fn),
176 }
177 }
178
179 /// Signs a transaction input to produce an unlocking script.
180 ///
181 /// # Arguments
182 ///
183 /// * `context` - The signing context with transaction data
184 ///
185 /// # Returns
186 ///
187 /// The unlocking script, or an error if signing fails.
188 pub fn sign(&self, context: &SigningContext) -> Result<UnlockingScript> {
189 (self.sign_fn)(context)
190 }
191
192 /// Estimates the unlocking script length in bytes.
193 ///
194 /// This is useful for fee estimation before the actual signature is created.
195 pub fn estimate_length(&self) -> usize {
196 (self.estimate_length_fn)()
197 }
198}
199
200impl std::fmt::Debug for ScriptTemplateUnlock {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 f.debug_struct("ScriptTemplateUnlock")
203 .field("estimate_length", &self.estimate_length())
204 .finish_non_exhaustive()
205 }
206}
207
208/// Helper function to create a transaction signature.
209///
210/// Signs the sighash with the private key and returns a TransactionSignature.
211pub fn create_transaction_signature(
212 private_key: &PrivateKey,
213 sighash: &[u8; 32],
214 scope: u32,
215) -> Result<TransactionSignature> {
216 let signature = private_key.sign(sighash)?;
217 Ok(TransactionSignature::new(signature, scope))
218}
219
220/// Helper function to build an unlocking script from signature and public key.
221///
222/// Creates an unlocking script with the signature in checksig format followed
223/// by the compressed public key.
224pub fn build_p2pkh_unlocking_script(
225 tx_sig: &TransactionSignature,
226 private_key: &PrivateKey,
227) -> UnlockingScript {
228 let sig_bytes = tx_sig.to_checksig_format();
229 let pubkey_bytes = private_key.public_key().to_compressed();
230
231 let mut script = Script::new();
232 script.write_bin(&sig_bytes).write_bin(&pubkey_bytes);
233
234 UnlockingScript::from_script(script)
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_sign_outputs_to_sighash() {
243 assert_eq!(SignOutputs::All.to_sighash_flag(), SIGHASH_ALL);
244 assert_eq!(SignOutputs::None.to_sighash_flag(), SIGHASH_NONE);
245 assert_eq!(SignOutputs::Single.to_sighash_flag(), SIGHASH_SINGLE);
246 }
247
248 #[test]
249 fn test_compute_sighash_scope() {
250 // Standard BSV signature: ALL | FORKID
251 assert_eq!(
252 compute_sighash_scope(SignOutputs::All, false),
253 SIGHASH_ALL | SIGHASH_FORKID
254 );
255
256 // With ANYONECANPAY
257 assert_eq!(
258 compute_sighash_scope(SignOutputs::All, true),
259 SIGHASH_ALL | SIGHASH_FORKID | SIGHASH_ANYONECANPAY
260 );
261
262 // NONE | FORKID
263 assert_eq!(
264 compute_sighash_scope(SignOutputs::None, false),
265 SIGHASH_NONE | SIGHASH_FORKID
266 );
267
268 // SINGLE | FORKID | ANYONECANPAY
269 assert_eq!(
270 compute_sighash_scope(SignOutputs::Single, true),
271 SIGHASH_SINGLE | SIGHASH_FORKID | SIGHASH_ANYONECANPAY
272 );
273 }
274}