This source candidate declares SDK peer ^2.1.6 || ^3.0.0. SDK3 remains
a coordinated proposal; see the qualification and migration limits
before adopting it.
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.
The 2.0.0 source candidate is a breaking release: MandalaToken, MandalaAdmin and ADMIN_PROTOCOL are removed, and BRC-162 token outputs and their strict CBOR payloads move to Bsv21Binary. The SDK peer range is unchanged in source, and 2.0.0 is the first release to publish ^3.0.0 alongside ^2.1.6.
The 1.10.3 source candidate fixes the CommonJS build: require('@bsv/templates') consumers can construct scripts again instead of failing with LockingScript.default is not a constructor (#571). The ESM build and browser bundle size are unchanged; no API migration is required.
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"}']bigint amounts, and a strict CBOR payload codec (encodeStrictCbor, decodeStrictCbor, tryDecodeStrictCbor)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 { PrivateKey } from '@bsv/sdk'
import { Bsv21Binary, encodeStrictCbor, tokenIdToString } from '@bsv/templates'
const pubKeyHash = PrivateKey.fromRandom().toPublicKey().toHash() as number[]
const template = new Bsv21Binary()
// A deploy output has a null token id and carries a strict-CBOR payload.
const deploy = template.lock(null, 0n, pubKeyHash, encodeStrictCbor({ sym: 'USD', dec: 2n }))
console.log(Bsv21Binary.decode(deploy).role) // 'deploy'
// Later outputs name the token by `<deploy txid>_0`; amounts are bigint.
const tokenId = `${'ab'.repeat(32)}_0`
const decoded = Bsv21Binary.decode(template.lock(tokenId, 1_000_000n, pubKeyHash))
console.log(decoded.role, decoded.amount) // 'value' 1000000n
console.log(decoded.tokenId !== undefined && tokenIdToString(decoded.tokenId) === tokenId) // truedecode throws Bsv21BinaryError for a script that starts <push> <push> OP_2DROP but is not canonical (for example amount 5 pushed as 01 05); isTokenShaped distinguishes that from an ordinary script. decodeStrictCbor throws StrictCborError on floats, tags, negative integers, arrays, unsorted keys, non-minimal headers, trailing bytes, nesting deeper than 4 or more than 4096 bytes; tryDecodeStrictCbor returns undefined instead.
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