Low-level BSV script templates library — provides reusable locking/unlocking script implementations (OpReturn, MultiPushDrop, P2MSKH) for common and advanced Bitcoin SV patterns without abstracting away control.
npm install @bsv/templatesimport { OpReturn } from '@bsv/templates'
const opReturn = new OpReturn()
const lockingScript = opReturn.lock(['APP', JSON.stringify({ action: 'vote' })])
console.log(lockingScript.toHex())
const decodedData = OpReturn.decode(lockingScript)
console.log(decodedData) // ['APP', '{"action":"vote"}']import { OpReturn } from '@bsv/templates'
const script = new OpReturn().lock(['my-app', 'invoice-paid'])
const fields = OpReturn.decode(script)
console.log(fields) // ['my-app', 'invoice-paid']import { SecurityLevel, Utils, type WalletInterface } from '@bsv/sdk'
import { MultiPushDrop } from '@bsv/templates'
declare const creatorWallet: WalletInterface
declare const ownerWallet: WalletInterface
const counterparties = [
'02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5',
'03f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a'
]
const protocolID: [SecurityLevel, string] = [0, 'example token']
const keyID = 'ticket-1'
const pushDrop = new MultiPushDrop(creatorWallet)
const lockingScript = await pushDrop.lock(
[Utils.toArray('ticket', 'utf8'), [1, 2, 3]],
protocolID,
keyID,
counterparties
)
const decoded = MultiPushDrop.decode(lockingScript)
console.log(decoded.lockingPublicKeys.length) // 2
// The first owner can build the unlock template for a spending transaction.
const { publicKey: creatorIdentityKey } = await creatorWallet.getPublicKey({ identityKey: true })
const unlocker = new MultiPushDrop(ownerWallet).unlock(protocolID, keyID, creatorIdentityKey)import { PublicKey, type WalletInterface } from '@bsv/sdk'
import { P2MSKH } from '@bsv/templates'
declare const wallet: WalletInterface
const pubkey1 = '02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'
const pubkey2 = '03f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a'
const pubkey3 = '02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'
const pubkeys = [
PublicKey.fromString(pubkey1),
PublicKey.fromString(pubkey2),
PublicKey.fromString(pubkey3)
]
const address = P2MSKH.address(pubkeys, 2)
const lockingScript = new P2MSKH().lock(address)
// Spending uses wallet-derived signatures. Each signer applies the same
// customInstructions and passes the prior partial unlocking script onward.
const customInstructions = {
keyID: 'escrow-1',
counterparty: 'self',
pubkeys: pubkeys.map(pubkey => pubkey.toString())
}
const unlocker = new P2MSKH().unlock(wallet, customInstructions)import { Hash, type PrivateKey, Utils } from '@bsv/sdk'
import { R1K1Wallet } from '@bsv/templates'
declare const compressedP256PublicKeyHex: string
declare const k1RecoveryPrivateKey: PrivateKey
declare const signWithYubiKeyPiv: (digest: Uint8Array) => Promise<Uint8Array>
const template = new R1K1Wallet()
const r1PublicKey = Utils.toArray(compressedP256PublicKeyHex, 'hex')
const salt = crypto.getRandomValues(new Uint8Array(32))
const lockingScript = await template.lock(
Hash.hash160([...r1PublicKey, ...salt]),
Hash.hash160(k1RecoveryPrivateKey.toPublicKey().encode(true) as number[])
)
const normalSpend = template.unlock({
path: 'r1',
publicKey: r1PublicKey,
salt,
signDigest: signWithYubiKeyPiv
})
const recoverySpend = template.unlock({
path: 'k1',
privateKey: k1RecoveryPrivateKey
})The R1 signer receives the final 32-byte transaction digest. A PIV adapter must
submit it unchanged to GENERAL AUTHENTICATE and return either the DER ECDSA
signature produced by the device or raw 64-byte r || s; applying SHA-256
again creates an invalid signature. Preserve each private 32-byte salt with the
wallet metadata. The salt hides a reused PIV public key only until the R1 output
is spent.
The generated locking script is 959,632 bytes after both commitments are baked, above common 500 KB miner policy. Confirm target-miner policy before funding it.
An R1 unlocking script also pushes the BIP-143 preimage, whose scriptCode
contains roughly 960 KB of the contract after OP_CODESEPARATOR. The R1 path
therefore involves about 2 MB of locking-plus-unlocking script material, and
the witness alone adds roughly 960 KB to the spending transaction. Account for
the resulting fees and confirm any maximum-transaction policy; estimateLength
includes this preimage push. The K1 unlocking script remains small.
PIV proves that the hardware key signed the supplied digest, but a YubiKey does not display or independently validate the Bitcoin transaction. PIN and touch policies protect key use, not transaction intent; review the transaction on a trusted host before approving it.
lock() to create locking script and unlock() to sign/spendunlock().sign() calls are async; use awaitenc: 'hex' for binary