Payment Channels
Complete examples for implementing payment channels on BSV, enabling instant, low-cost off-chain transactions with on-chain settlement guarantees.
Overview
Payment channels allow two parties to conduct multiple transactions off-chain while maintaining the security guarantees of on-chain settlement. By opening a channel with an initial funding transaction, parties can exchange signed state updates representing balance changes, and only broadcast the final state to the blockchain when closing the channel. This enables high-throughput, low-latency micropayments.
Related SDK Components:
Payment Channel Setup
import { Transaction, PrivateKey, PublicKey, Script, P2PKH, OP } from '@bsv/sdk'
/**
* Payment Channel Manager
*
* Manages the complete lifecycle of a unidirectional payment channel.
*/
class PaymentChannelManager {
/**
* Create a payment channel funding transaction
*
* @param sender - Channel sender (pays into channel)
* @param receiver - Channel receiver
* @param fundingAmount - Amount to lock in channel
* @param duration - Channel duration in seconds
* @param utxo - UTXO to fund the channel
* @returns Channel setup details
*/
async createChannel(
sender: PrivateKey,
receiver: PublicKey,
fundingAmount: number,
duration: number,
utxo: {
txid: string
vout: number
satoshis: number
script: Script
}
): Promise<{
channelId: string
fundingTx: Transaction
expiryTime: number
initialBalance: {
sender: number
receiver: number
}
}> {
try {
if (fundingAmount < 1000) {
throw new Error('Funding amount too small for channel')
}
if (fundingAmount > utxo.satoshis) {
throw new Error('Insufficient funds in UTXO')
}
// Calculate expiry time
const currentTime = Math.floor(Date.now() / 1000)
const expiryTime = currentTime + duration
// Create funding transaction
const fundingTx = new Transaction()
fundingTx.addInput({
sourceTXID: utxo.txid,
sourceOutputIndex: utxo.vout,
unlockingScriptTemplate: new P2PKH().unlock(sender),
sequence: 0xffffffff
})
// Create 2-of-2 multisig output for channel
const channelScript = this.createChannelScript(
sender.toPublicKey(),
receiver,
expiryTime
)
const fee = 500
const channelAmount = fundingAmount - fee
fundingTx.addOutput({
satoshis: channelAmount,
lockingScript: channelScript
})
// Add change output if needed
const change = utxo.satoshis - fundingAmount - fee
if (change > 546) {
fundingTx.addOutput({
satoshis: change,
lockingScript: new P2PKH().lock(sender.toPublicKey().toHash())
})
}
await fundingTx.sign()
const channelId = fundingTx.id('hex')
console.log('Payment channel created')
console.log('Channel ID:', channelId)
console.log('Capacity:', channelAmount, 'satoshis')
console.log('Expires:', new Date(expiryTime * 1000).toISOString())
return {
channelId,
fundingTx,
expiryTime,
initialBalance: {
sender: channelAmount,
receiver: 0
}
}
} catch (error) {
throw new Error(`Channel creation failed: ${error.message}`)
}
}
/**
* Create channel locking script
*
* Allows spending with both signatures or sender signature after timeout
*/
private createChannelScript(
senderPubKey: PublicKey,
receiverPubKey: PublicKey,
expiryTime: number
): Script {
// Channel script:
// IF
// 2 <senderPubKey> <receiverPubKey> 2 OP_CHECKMULTISIG
// ELSE
// <expiryTime> OP_CHECKLOCKTIMEVERIFY OP_DROP <senderPubKey> OP_CHECKSIG
// ENDIF
const script = new Script()
// IF branch (cooperative close with both signatures)
script.chunks.push({ op: OP.OP_IF })
script.chunks.push({ op: OP.OP_2 })
script.chunks.push({ data: senderPubKey.toBuffer() })
script.chunks.push({ data: receiverPubKey.toBuffer() })
script.chunks.push({ op: OP.OP_2 })
script.chunks.push({ op: OP.OP_CHECKMULTISIG })
// ELSE branch (unilateral close after timeout)
script.chunks.push({ op: OP.OP_ELSE })
script.chunks.push({ data: this.numberToBuffer(expiryTime) })
script.chunks.push({ op: OP.OP_CHECKLOCKTIMEVERIFY })
script.chunks.push({ op: OP.OP_DROP })
script.chunks.push({ data: senderPubKey.toBuffer() })
script.chunks.push({ op: OP.OP_CHECKSIG })
script.chunks.push({ op: OP.OP_ENDIF })
return script
}
/**
* Convert number to buffer for script
*/
private numberToBuffer(num: number): Buffer {
if (num === 0) return Buffer.from([])
const isNegative = num < 0
const absNum = Math.abs(num)
const bytes: number[] = []
let n = absNum
while (n > 0) {
bytes.push(n & 0xff)
n >>= 8
}
if (bytes[bytes.length - 1] & 0x80) {
bytes.push(isNegative ? 0x80 : 0x00)
} else if (isNegative) {
bytes[bytes.length - 1] |= 0x80
}
return Buffer.from(bytes)
}
}
/**
* Usage Example
*/
async function setupChannelExample() {
const manager = new PaymentChannelManager()
const sender = PrivateKey.fromRandom()
const receiver = PrivateKey.fromRandom()
const utxo = {
txid: 'funding-utxo...',
vout: 0,
satoshis: 1000000,
script: new P2PKH().lock(sender.toPublicKey().toHash())
}
// Create channel with 500,000 sats for 24 hours
const channel = await manager.createChannel(
sender,
receiver.toPublicKey(),
500000,
24 * 60 * 60,
utxo
)
console.log('Channel ready for payments')
}Channel State Updates
Channel Closing
Bidirectional Payment Channel
Related Examples
See Also
SDK Components:
Transaction - Core transaction building
Script - Bitcoin script operations
Script Templates - Custom script templates
Signatures - Digital signatures
Learning Paths:
Last updated
