Script Templates
Overview
Script Templates in the BSV TypeScript SDK provide standardized, reusable patterns for creating locking and unlocking scripts. The ScriptTemplate interface and its implementations (like P2PKH) abstract away the complexity of Bitcoin Script construction, making it easy to create secure, standard transaction types. Templates ensure correct script generation and reduce errors in transaction building.
Purpose
Provide pre-built templates for standard transaction types (P2PKH, P2PK, etc.)
Abstract Bitcoin Script complexity with simple, type-safe interfaces
Enable creation of both locking scripts (outputs) and unlocking script templates (inputs)
Support custom template creation for advanced use cases
Ensure BRC compliance and interoperability
Simplify transaction signing with automatic script generation
Reduce errors through tested, standard implementations
Basic Usage
import { Transaction, P2PKH, PrivateKey } from '@bsv/sdk';
const privKey = PrivateKey.fromWif('L5EY1SbTvvPNSdCYQe1EJHfXCBBT4PmnF6CDbzCm9iifZptUvDGB');
const recipientAddress = '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa';
// Create P2PKH template instance
const p2pkh = new P2PKH();
const tx = new Transaction();
// Create locking script for output
const lockingScript = p2pkh.lock(recipientAddress);
tx.addOutput({
lockingScript,
satoshis: 10000
});
// Create unlocking script template for input
const unlockingTemplate = p2pkh.unlock(privKey);
tx.addInput({
sourceTXID: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b',
sourceOutputIndex: 0,
unlockingScriptTemplate: unlockingTemplate
});
// Sign transaction (unlocking script generated automatically)
await tx.sign();
console.log('Transaction created with P2PKH template');Key Features
1. P2PKH Template (Pay to Public Key Hash)
The most common Bitcoin transaction template:
import { Transaction, P2PKH, PrivateKey, PublicKey } from '@bsv/sdk';
const p2pkh = new P2PKH();
// Create locking script from address
const address = '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa';
const lockingScript = p2pkh.lock(address);
console.log('Locking script:', lockingScript.toASM());
// Output: "OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG"
// Create locking script from public key
const pubKey = PrivateKey.fromRandom().toPublicKey();
const lockingScriptFromPubKey = p2pkh.lock(pubKey.toAddress());
// Create unlocking script template (default: SIGHASH_ALL)
const privKey = PrivateKey.fromWif('L5EY1SbTvvPNSdCYQe1EJHfXCBBT4PmnF6CDbzCm9iifZptUvDGB');
const unlockingTemplate = p2pkh.unlock(privKey);
// Create unlocking template with custom sighash
const customUnlocking = p2pkh.unlock(
privKey,
'single', // signOutputs: 'all' | 'none' | 'single'
true, // anyoneCanPay: allows others to add inputs
10000, // sourceSatoshis (optional)
lockingScript // lockingScript (optional)
);
// Use in transaction
const tx = new Transaction();
tx.addInput({
sourceTXID: '4a5e...',
sourceOutputIndex: 0,
sourceSatoshis: 10000,
unlockingScriptTemplate: unlockingTemplate
});
tx.addOutput({
lockingScript: p2pkh.lock(address),
satoshis: 9000
});
tx.addOutput({
lockingScript: p2pkh.lock(privKey.toPublicKey().toAddress()),
change: true
});
await tx.fee();
await tx.sign();
console.log('P2PKH transaction signed');2. Custom Script Templates
Create your own templates for advanced use cases:
import {
ScriptTemplate,
LockingScript,
UnlockingScript,
Script,
OP,
Transaction,
Hash
} from '@bsv/sdk';
/**
* Hash Puzzle Template
* Anyone who knows the secret can unlock
*/
class HashPuzzle implements ScriptTemplate {
/**
* Create locking script with hash of secret
*/
lock(secretHash: Buffer): LockingScript {
return new Script()
.writeOpCode(OP.OP_HASH256)
.writeBin(secretHash)
.writeOpCode(OP.OP_EQUAL);
}
/**
* Create unlocking script template with secret
*/
unlock(secret: Buffer): {
sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>;
estimateLength: (tx: Transaction, inputIndex: number) => Promise<number>;
} {
return {
sign: async (tx: Transaction, inputIndex: number) => {
// Simply push the secret onto the stack
return new Script().writeBin(secret);
},
estimateLength: async () => {
// Length of secret + push opcode
return secret.length + 2;
}
};
}
}
// Usage
const secret = Buffer.from('my_secret_value');
const secretHash = Hash.sha256(secret);
const hashPuzzle = new HashPuzzle();
const tx = new Transaction();
// Create output with hash puzzle
tx.addOutput({
lockingScript: hashPuzzle.lock(secretHash),
satoshis: 10000
});
// Later, unlock with secret
tx.addInput({
sourceTXID: '...',
sourceOutputIndex: 0,
unlockingScriptTemplate: hashPuzzle.unlock(secret)
});
await tx.sign();
console.log('Hash puzzle unlocked');3. Time-Locked Template
Create template with time-based conditions:
import {
ScriptTemplate,
Script,
OP,
Transaction,
PrivateKey,
Signature,
Hash
} from '@bsv/sdk';
/**
* CheckLockTimeVerify (CLTV) Template
* Can only spend after specified time/block height
*/
class TimeLock implements ScriptTemplate {
/**
* Create time-locked locking script
*/
lock(lockTime: number, pubKeyHash: Buffer): LockingScript {
const lockTimeBuffer = Buffer.alloc(4);
lockTimeBuffer.writeUInt32LE(lockTime, 0);
return new Script()
// Check locktime
.writeBin(lockTimeBuffer)
.writeOpCode(OP.OP_CHECKLOCKTIMEVERIFY)
.writeOpCode(OP.OP_DROP)
// Standard P2PKH after locktime check
.writeOpCode(OP.OP_DUP)
.writeOpCode(OP.OP_HASH160)
.writeBin(pubKeyHash)
.writeOpCode(OP.OP_EQUALVERIFY)
.writeOpCode(OP.OP_CHECKSIG);
}
/**
* Create unlocking template
*/
unlock(
privKey: PrivateKey,
lockTime: number
): {
sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>;
estimateLength: (tx: Transaction, inputIndex: number) => Promise<number>;
} {
return {
sign: async (tx: Transaction, inputIndex: number) => {
// Set transaction locktime
tx.lockTime = lockTime;
// Get sighash and sign
const preimage = tx.getPreimage(inputIndex);
const hash = Hash.sha256sha256(Array.from(preimage));
const signature = Signature.sign(hash, privKey);
// Create unlocking script: <sig> <pubkey>
return new Script()
.writeBin(signature.toChecksigFormat())
.writeBin(privKey.toPublicKey().encode(true));
},
estimateLength: async () => {
return 107; // Typical signature + pubkey size
}
};
}
}
// Usage: Lock coins until specific block height
const lockTime = 800000; // Block height
const pubKeyHash = Hash.hash160(privKey.toPublicKey().encode(true));
const timeLock = new TimeLock();
const tx = new Transaction();
tx.addOutput({
lockingScript: timeLock.lock(lockTime, pubKeyHash),
satoshis: 100000
});
// Later, after block 800000
const unlockTx = new Transaction();
unlockTx.addInput({
sourceTXID: tx.id('hex'),
sourceOutputIndex: 0,
unlockingScriptTemplate: timeLock.unlock(privKey, lockTime)
});
await unlockTx.sign();
console.log('Time-locked funds unlocked');4. Multi-Signature Template
M-of-N multisig template:
import {
ScriptTemplate,
Script,
OP,
Transaction,
PrivateKey,
PublicKey,
Signature,
Hash
} from '@bsv/sdk';
/**
* Multi-Signature Template (M-of-N)
*/
class MultiSig implements ScriptTemplate {
/**
* Create M-of-N multisig locking script
*/
lock(requiredSigs: number, publicKeys: PublicKey[]): LockingScript {
const script = new Script()
.writeNumber(requiredSigs);
// Add all public keys
publicKeys.forEach(pubKey => {
script.writeBin(pubKey.encode(true));
});
script
.writeNumber(publicKeys.length)
.writeOpCode(OP.OP_CHECKMULTISIG);
return script;
}
/**
* Create unlocking template with M private keys
*/
unlock(
privateKeys: PrivateKey[],
requiredSigs: number
): {
sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>;
estimateLength: (tx: Transaction, inputIndex: number) => Promise<number>;
} {
if (privateKeys.length < requiredSigs) {
throw new Error(`Need at least ${requiredSigs} private keys`);
}
return {
sign: async (tx: Transaction, inputIndex: number) => {
// Get sighash
const preimage = tx.getPreimage(inputIndex);
const hash = Hash.sha256sha256(Array.from(preimage));
// Create signatures
const signatures = privateKeys
.slice(0, requiredSigs)
.map(privKey => Signature.sign(hash, privKey));
// Create unlocking script: OP_0 <sig1> <sig2> ... <sigM>
const script = new Script()
.writeOpCode(OP.OP_0); // Bug in CHECKMULTISIG requires extra value
signatures.forEach(sig => {
script.writeBin(sig.toChecksigFormat());
});
return script;
},
estimateLength: async () => {
// OP_0 + M signatures (73 bytes each average)
return 1 + (requiredSigs * 73);
}
};
}
}
// Usage: 2-of-3 multisig
const privKeys = [
PrivateKey.fromRandom(),
PrivateKey.fromRandom(),
PrivateKey.fromRandom()
];
const pubKeys = privKeys.map(pk => pk.toPublicKey());
const multiSig = new MultiSig();
// Create 2-of-3 multisig output
const tx = new Transaction();
tx.addOutput({
lockingScript: multiSig.lock(2, pubKeys),
satoshis: 100000
});
// Later, unlock with 2 of the 3 keys
const unlockTx = new Transaction();
unlockTx.addInput({
sourceTXID: tx.id('hex'),
sourceOutputIndex: 0,
unlockingScriptTemplate: multiSig.unlock([privKeys[0], privKeys[2]], 2)
});
await unlockTx.sign();
console.log('2-of-3 multisig unlocked');API Reference
ScriptTemplate Interface
interface ScriptTemplate {
/**
* Create locking script (for outputs)
*/
lock(...args: any[]): LockingScript;
/**
* Create unlocking script template (for inputs)
*/
unlock(...args: any[]): {
sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>;
estimateLength: (tx: Transaction, inputIndex: number) => Promise<number>;
};
}P2PKH Template
class P2PKH implements ScriptTemplate {
// Create locking script from address or public key
lock(address: string): LockingScript;
// Create unlocking script template
unlock(
privateKey: PrivateKey,
signOutputs?: 'all' | 'none' | 'single',
anyoneCanPay?: boolean,
sourceSatoshis?: number,
lockingScript?: Script
): UnlockingScriptTemplate;
}Creating Custom Templates
class CustomTemplate implements ScriptTemplate {
lock(param1: any, param2: any): LockingScript {
// Return a Script that defines spending conditions
return new Script()
.writeOpCode(OP.OP_...)
.writeBin(data);
}
unlock(param1: any): UnlockingScriptTemplate {
return {
sign: async (tx: Transaction, inputIndex: number) => {
// Generate unlocking script based on transaction context
// Typically involves creating signatures
return new Script()
.writeBin(signature)
.writeBin(data);
},
estimateLength: async (tx: Transaction, inputIndex: number) => {
// Return estimated byte length of unlocking script
return estimatedLength;
}
};
}
}Common Patterns
Pattern 1: Reusable Template Factory
Create template instances with configuration:
import { P2PKH, PrivateKey, Transaction } from '@bsv/sdk';
class TemplateFactory {
/**
* Create configured P2PKH template
*/
static createP2PKH(options?: {
signOutputs?: 'all' | 'none' | 'single';
anyoneCanPay?: boolean;
}) {
return {
template: new P2PKH(),
options: {
signOutputs: options?.signOutputs || 'all',
anyoneCanPay: options?.anyoneCanPay || false
}
};
}
/**
* Create payment transaction with template
*/
static async createPayment(
from: {
txid: string;
vout: number;
satoshis: number;
privKey: PrivateKey
},
to: { address: string; satoshis: number },
change: { address: string }
): Promise<Transaction> {
const p2pkh = new P2PKH();
const tx = new Transaction();
// Add input with P2PKH unlocking
tx.addInput({
sourceTXID: from.txid,
sourceOutputIndex: from.vout,
sourceSatoshis: from.satoshis,
unlockingScriptTemplate: p2pkh.unlock(from.privKey)
});
// Add payment output
tx.addOutput({
lockingScript: p2pkh.lock(to.address),
satoshis: to.satoshis
});
// Add change output
tx.addOutput({
lockingScript: p2pkh.lock(change.address),
change: true
});
await tx.fee();
await tx.sign();
return tx;
}
}
// Usage
const payment = await TemplateFactory.createPayment(
{
txid: '4a5e...',
vout: 0,
satoshis: 100000,
privKey: PrivateKey.fromWif('L5EY...')
},
{ address: '1A1z...', satoshis: 50000 },
{ address: '1B2y...' }
);Pattern 2: Template Composition
Combine multiple templates:
import { ScriptTemplate, Script, OP, Transaction } from '@bsv/sdk';
/**
* Composite template combining multiple conditions
*/
class CompositeTemplate implements ScriptTemplate {
constructor(
private templates: ScriptTemplate[],
private mode: 'AND' | 'OR' = 'AND'
) {}
lock(...args: any[]): LockingScript {
const scripts = this.templates.map((t, i) => t.lock(args[i]));
if (this.mode === 'OR') {
// IF condition1 ELSE IF condition2 ELSE ... ENDIF
const composite = new Script();
scripts.forEach((script, index) => {
if (index > 0) composite.writeOpCode(OP.OP_ELSE);
composite.writeOpCode(OP.OP_IF);
scripts.forEach(chunk => composite.writeOpCode(chunk.op));
});
composite.writeOpCode(OP.OP_ENDIF);
return composite;
} else {
// AND: All conditions must be met
const composite = new Script();
scripts.forEach(script => {
script.chunks.forEach(chunk => {
if (chunk.data) {
composite.writeBin(chunk.data);
} else {
composite.writeOpCode(chunk.op);
}
});
});
return composite;
}
}
unlock(...args: any[]): UnlockingScriptTemplate {
// Implementation depends on composition logic
throw new Error('Composite unlock not implemented');
}
}
// Usage: Combine hash puzzle with time lock
const composite = new CompositeTemplate(
[
new HashPuzzle(),
new TimeLock()
],
'AND'
);Pattern 3: Protocol-Specific Templates
Create templates for specific protocols:
import { ScriptTemplate, Script, OP, P2PKH } from '@bsv/sdk';
/**
* BRC-20 Token Template (simplified example)
*/
class TokenTemplate {
private p2pkh = new P2PKH();
/**
* Create token output with embedded token data
*/
createTokenOutput(
tokenId: Buffer,
amount: bigint,
ownerAddress: string,
satoshis: number
): TransactionOutput {
// Token metadata in OP_RETURN
const tokenData = new Script()
.writeOpCode(OP.OP_FALSE)
.writeOpCode(OP.OP_RETURN)
.writeBin(Buffer.from('TOKEN'))
.writeBin(tokenId)
.writeBin(Buffer.from(amount.toString()));
// Standard P2PKH for ownership
const ownershipScript = this.p2pkh.lock(ownerAddress);
return {
lockingScript: ownershipScript,
satoshis
};
}
/**
* Create token transfer transaction
*/
async transferToken(
tokenUTXO: {
txid: string;
vout: number;
satoshis: number;
tokenId: Buffer;
amount: bigint;
},
fromPrivKey: PrivateKey,
toAddress: string
): Promise<Transaction> {
const tx = new Transaction();
// Input: Spend token UTXO
tx.addInput({
sourceTXID: tokenUTXO.txid,
sourceOutputIndex: tokenUTXO.vout,
sourceSatoshis: tokenUTXO.satoshis,
unlockingScriptTemplate: this.p2pkh.unlock(fromPrivKey)
});
// Output: Token to new owner
tx.addOutput(
this.createTokenOutput(
tokenUTXO.tokenId,
tokenUTXO.amount,
toAddress,
tokenUTXO.satoshis
)
);
await tx.fee();
await tx.sign();
return tx;
}
}
// Usage
const tokenTemplate = new TokenTemplate();
const tokenId = Buffer.from('TOKEN_ID_123');
// Create token
const output = tokenTemplate.createTokenOutput(
tokenId,
1000n,
'1A1z...',
1000
);
// Transfer token
const transferTx = await tokenTemplate.transferToken(
{
txid: '4a5e...',
vout: 0,
satoshis: 1000,
tokenId,
amount: 1000n
},
PrivateKey.fromWif('L5EY...'),
'1B2y...'
);Security Considerations
Template Validation: Always validate template parameters before using in production.
Sighash Type Selection: Choose appropriate sighash types for your use case. SIGHASH_ALL is most secure.
Private Key Protection: Never log or expose private keys used in unlock templates.
Custom Template Testing: Thoroughly test custom templates with small amounts before production use.
Script Size Limits: Ensure generated scripts stay within protocol limits.
Standard Compliance: Use standard templates (P2PKH) when possible for maximum compatibility.
Performance Considerations
Template Reuse: Create template instances once and reuse them across multiple transactions.
Unlocking Script Size: Simpler unlock templates result in smaller transactions and lower fees.
Estimation Accuracy: Accurate
estimateLengthimplementations help with fee calculation.Signature Caching: For repeated signatures, consider caching when safe to do so.
Related Components
Transaction - Use templates in transactions
Script - Low-level script building
TransactionInput - Use unlock templates
TransactionOutput - Use lock templates
Signature - Generate signatures for unlocking
Code Examples
See complete working examples in:
Best Practices
Use standard templates (P2PKH) for maximum compatibility
Test custom templates thoroughly before production use
Document template parameters clearly for maintainability
Implement proper error handling in unlock functions
Provide accurate length estimates for fee calculation
Follow BRC standards for protocol-specific templates
Keep templates simple to minimize errors and fees
Reuse template instances across transactions
Validate all inputs before script generation
Include comprehensive unit tests for custom templates
Troubleshooting
Issue: Template unlock fails during signing
Solution: Verify template parameters match the locking script.
// Ensure unlock template matches lock type
const p2pkh = new P2PKH();
const lockingScript = p2pkh.lock(address);
// Use same template for unlocking
const unlockingTemplate = p2pkh.unlock(privKey);Issue: Custom template generates invalid script
Solution: Validate script structure and opcodes.
class CustomTemplate implements ScriptTemplate {
lock(data: Buffer): LockingScript {
const script = new Script()
.writeOpCode(OP.OP_HASH256)
.writeBin(data)
.writeOpCode(OP.OP_EQUAL);
// Validate script
const asm = script.toASM();
console.log('Generated script:', asm);
return script;
}
}Issue: Unlock template estimation inaccurate
Solution: Implement precise length estimation.
estimateLength: async (tx: Transaction, inputIndex: number) => {
// Account for all elements
const sigLength = 73; // Max DER signature length
const pubKeyLength = 33; // Compressed public key
const pushOpcodes = 2; // OP_PUSHDATA opcodes
return sigLength + pubKeyLength + pushOpcodes;
}Issue: Template not working with BRC protocol
Solution: Ensure compliance with BRC specifications.
// Follow BRC-29 for simple payment protocol
class BRC29Template extends P2PKH {
// Implement BRC-29 specific features
}Further Reading
Script Templates - Standard Bitcoin scripts
P2PKH - Pay to public key hash
BRC-29 - Simple payment protocol
Custom Scripts - Script examples
BSV SDK Documentation - Official SDK docs
Status
✅ Complete
Last updated
