Smart Contracts
Overview
Escrow Contract
import { Transaction, PrivateKey, PublicKey, Script, Hash, OP, P2PKH } from '@bsv/sdk'
/**
* Escrow Smart Contract
*
* Implements a trustless escrow system where funds are released
* based on signatures from buyer, seller, and optional arbiter.
*/
class EscrowContract {
/**
* Create an escrow locking script
*
* Funds can be released in three ways:
* 1. Buyer + Seller signatures (happy path)
* 2. Buyer + Arbiter signatures (dispute resolution in buyer's favor)
* 3. Seller + Arbiter signatures (dispute resolution in seller's favor)
*
* @param buyerPubKey - Public key of the buyer
* @param sellerPubKey - Public key of the seller
* @param arbiterPubKey - Public key of the arbiter
* @param timeout - Timeout after which seller can claim (unix timestamp)
* @returns Escrow locking script
*/
createEscrowScript(
buyerPubKey: PublicKey,
sellerPubKey: PublicKey,
arbiterPubKey: PublicKey,
timeout?: number
): Script {
try {
// 2-of-3 multisig with optional timeout
const script = new Script()
if (timeout) {
// If timeout is specified, add timeout branch
script.chunks.push({ op: OP.OP_IF })
}
// 2-of-3 multisig: any two of buyer, seller, arbiter
script.chunks.push({ op: OP.OP_2 })
script.chunks.push({ data: buyerPubKey.toBuffer() })
script.chunks.push({ data: sellerPubKey.toBuffer() })
script.chunks.push({ data: arbiterPubKey.toBuffer() })
script.chunks.push({ op: OP.OP_3 })
script.chunks.push({ op: OP.OP_CHECKMULTISIG })
if (timeout) {
// Timeout branch: seller can claim after timeout
script.chunks.push({ op: OP.OP_ELSE })
script.chunks.push({ data: this.numberToBuffer(timeout) })
script.chunks.push({ op: OP.OP_CHECKLOCKTIMEVERIFY })
script.chunks.push({ op: OP.OP_DROP })
script.chunks.push({ data: sellerPubKey.toBuffer() })
script.chunks.push({ op: OP.OP_CHECKSIG })
script.chunks.push({ op: OP.OP_ENDIF })
}
return script
} catch (error) {
throw new Error(`Escrow script creation failed: ${error.message}`)
}
}
/**
* Create escrow transaction
*
* @param buyer - Buyer's private key
* @param seller - Seller's public key
* @param arbiter - Arbiter's public key
* @param amount - Escrow amount
* @param utxo - Funding UTXO
* @param timeoutDays - Optional timeout in days
* @returns Escrow transaction and script
*/
async createEscrow(
buyer: PrivateKey,
seller: PublicKey,
arbiter: PublicKey,
amount: number,
utxo: {
txid: string
vout: number
satoshis: number
script: Script
},
timeoutDays?: number
): Promise<{
transaction: Transaction
escrowScript: Script
escrowDetails: {
amount: number
timeout?: number
participants: {
buyer: string
seller: string
arbiter: string
}
}
}> {
try {
if (amount < 1000) {
throw new Error('Escrow amount too small')
}
const timeout = timeoutDays
? Math.floor(Date.now() / 1000) + (timeoutDays * 24 * 60 * 60)
: undefined
// Create escrow script
const escrowScript = this.createEscrowScript(
buyer.toPublicKey(),
seller,
arbiter,
timeout
)
// Create funding transaction
const tx = new Transaction()
tx.addInput({
sourceTXID: utxo.txid,
sourceOutputIndex: utxo.vout,
unlockingScriptTemplate: new P2PKH().unlock(buyer),
sequence: 0xffffffff
})
const fee = 500
const escrowAmount = amount - fee
tx.addOutput({
satoshis: escrowAmount,
lockingScript: escrowScript
})
// Change output
const change = utxo.satoshis - amount - fee
if (change > 546) {
tx.addOutput({
satoshis: change,
lockingScript: new P2PKH().lock(buyer.toPublicKey().toHash())
})
}
await tx.sign()
console.log('Escrow created')
console.log(` Amount: ${escrowAmount} satoshis`)
console.log(` Timeout: ${timeout ? new Date(timeout * 1000).toISOString() : 'None'}`)
return {
transaction: tx,
escrowScript,
escrowDetails: {
amount: escrowAmount,
timeout,
participants: {
buyer: buyer.toPublicKey().toString(),
seller: seller.toString(),
arbiter: arbiter.toString()
}
}
}
} catch (error) {
throw new Error(`Escrow creation failed: ${error.message}`)
}
}
/**
* Release escrow funds (buyer + seller agreement)
*
* @param buyer - Buyer's private key
* @param seller - Seller's private key
* @param escrowUTXO - Escrow UTXO
* @param escrowScript - Escrow locking script
* @param recipient - Recipient address
* @returns Release transaction
*/
async releaseEscrow(
buyer: PrivateKey,
seller: PrivateKey,
escrowUTXO: {
txid: string
vout: number
satoshis: number
},
escrowScript: Script,
recipient: string
): Promise<Transaction> {
try {
console.log('Releasing escrow with buyer + seller signatures...')
const tx = new Transaction()
tx.addInput({
sourceTXID: escrowUTXO.txid,
sourceOutputIndex: escrowUTXO.vout,
unlockingScriptTemplate: {
sign: async (transaction: Transaction, inputIndex: number) => {
// Create signatures from both parties
const buyerSig = transaction.sign(inputIndex, buyer, escrowScript)
const sellerSig = transaction.sign(inputIndex, seller, escrowScript)
// Build unlocking script with both signatures
const unlockScript = new Script()
unlockScript.chunks.push({ op: OP.OP_0 }) // OP_CHECKMULTISIG bug
unlockScript.chunks.push({ data: buyerSig })
unlockScript.chunks.push({ data: sellerSig })
return unlockScript
},
estimateLength: () => 150
},
sequence: 0xffffffff
})
// Output to recipient
const fee = 500
const releaseAmount = escrowUTXO.satoshis - fee
tx.addOutput({
satoshis: releaseAmount,
lockingScript: Script.fromAddress(recipient)
})
await tx.sign()
console.log('Escrow released')
console.log(` Amount: ${releaseAmount} to ${recipient}`)
return tx
} catch (error) {
throw new Error(`Escrow release failed: ${error.message}`)
}
}
/**
* Resolve escrow dispute (arbiter involved)
*
* @param party - Either buyer or seller
* @param arbiter - Arbiter's private key
* @param escrowUTXO - Escrow UTXO
* @param escrowScript - Escrow locking script
* @param recipient - Recipient address
* @returns Resolution transaction
*/
async resolveEscrowDispute(
party: PrivateKey,
arbiter: PrivateKey,
escrowUTXO: {
txid: string
vout: number
satoshis: number
},
escrowScript: Script,
recipient: string
): Promise<Transaction> {
try {
console.log('Resolving escrow dispute with arbiter...')
const tx = new Transaction()
tx.addInput({
sourceTXID: escrowUTXO.txid,
sourceOutputIndex: escrowUTXO.vout,
unlockingScriptTemplate: {
sign: async (transaction: Transaction, inputIndex: number) => {
const partySig = transaction.sign(inputIndex, party, escrowScript)
const arbiterSig = transaction.sign(inputIndex, arbiter, escrowScript)
const unlockScript = new Script()
unlockScript.chunks.push({ op: OP.OP_0 })
unlockScript.chunks.push({ data: partySig })
unlockScript.chunks.push({ data: arbiterSig })
return unlockScript
},
estimateLength: () => 150
},
sequence: 0xffffffff
})
const fee = 500
const resolutionAmount = escrowUTXO.satoshis - fee
tx.addOutput({
satoshis: resolutionAmount,
lockingScript: Script.fromAddress(recipient)
})
await tx.sign()
console.log('Dispute resolved')
console.log(` Amount: ${resolutionAmount} to ${recipient}`)
return tx
} catch (error) {
throw new Error(`Dispute resolution failed: ${error.message}`)
}
}
/**
* 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 escrowExample() {
const escrow = new EscrowContract()
const buyer = PrivateKey.fromRandom()
const seller = PrivateKey.fromRandom()
const arbiter = PrivateKey.fromRandom()
const buyerUTXO = {
txid: 'buyer-utxo...',
vout: 0,
satoshis: 100000,
script: new P2PKH().lock(buyer.toPublicKey().toHash())
}
// Create escrow for 50,000 sats with 30-day timeout
const { transaction, escrowScript } = await escrow.createEscrow(
buyer,
seller.toPublicKey(),
arbiter.toPublicKey(),
50000,
buyerUTXO,
30
)
console.log('Escrow Transaction:', transaction.id('hex'))
// Happy path: buyer and seller agree to release
const releaseTx = await escrow.releaseEscrow(
buyer,
seller,
{
txid: transaction.id('hex'),
vout: 0,
satoshis: 49500
},
escrowScript,
seller.toPublicKey().toAddress()
)
console.log('Release Transaction:', releaseTx.id('hex'))
}Token Vesting Contract
Oracle-Based Conditional Payment
Multi-Party Contract
Related Examples
See Also
Last updated
