Smart Contracts

Complete examples for implementing smart contracts on BSV, including escrow services, token vesting schedules, and oracle-based conditional payments.

Overview

Smart contracts on BSV use Bitcoin Script to encode business logic and conditions directly into transactions. Unlike account-based blockchains, BSV smart contracts are UTXO-based, with conditions checked during transaction validation. This section demonstrates practical smart contract implementations for common use cases including escrow, vesting, and oracle integrations.

Related SDK Components:

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

See Also

SDK Components:

Learning Paths:

Last updated