Skip to main content

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}