Payment Channels
Overview
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
Last updated
