For the complete documentation index, see llms.txt. This page is also available as Markdown.

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