Batch Operations

Complete examples for performing batch payment processing and batch UTXO operations efficiently using the BSV SDK.

Overview

Batch operations allow you to process multiple payments or UTXO operations in a single transaction, significantly reducing fees and improving efficiency. This code feature demonstrates practical patterns for batch processing, from simple multi-recipient payments to complex UTXO consolidation and distribution strategies.

Related SDK Components:

Batch Payment Processor

import { Transaction, PrivateKey, P2PKH, Script } from '@bsv/sdk'

/**
 * Batch Payment Processor
 *
 * Efficiently process multiple payments in a single transaction,
 * reducing fees and blockchain footprint.
 */
class BatchPaymentProcessor {
  private readonly feePerByte = 0.5

  /**
   * Process batch payments to multiple recipients
   *
   * @param privateKey - Private key to sign the transaction
   * @param utxos - Available UTXOs to fund the payments
   * @param recipients - Array of recipients with addresses and amounts
   * @returns Signed transaction ready for broadcasting
   */
  async processBatchPayment(
    privateKey: PrivateKey,
    utxos: Array<{
      txid: string
      vout: number
      satoshis: number
      script: Script
    }>,
    recipients: Array<{
      address: string
      amount: number
      memo?: string
    }>
  ): Promise<Transaction> {
    try {
      // Validate recipients
      if (recipients.length === 0) {
        throw new Error('At least one recipient is required')
      }

      // Calculate total payment amount
      const totalPayment = recipients.reduce((sum, r) => sum + r.amount, 0)

      // Estimate transaction size and fee
      const estimatedSize = this.estimateTransactionSize(
        utxos.length,
        recipients.length + 1 // +1 for change output
      )
      const estimatedFee = Math.ceil(estimatedSize * this.feePerByte)

      // Select UTXOs to cover payment + fees
      const selectedUTXOs = this.selectUTXOs(utxos, totalPayment + estimatedFee)
      const totalInput = selectedUTXOs.reduce((sum, u) => sum + u.satoshis, 0)

      // Create transaction
      const tx = new Transaction()

      // Add inputs
      for (const utxo of selectedUTXOs) {
        tx.addInput({
          sourceTXID: utxo.txid,
          sourceOutputIndex: utxo.vout,
          unlockingScriptTemplate: new P2PKH().unlock(privateKey),
          sequence: 0xffffffff
        })
      }

      // Add recipient outputs
      for (const recipient of recipients) {
        // Validate amount is above dust threshold
        if (recipient.amount < 546) {
          throw new Error(`Amount ${recipient.amount} is below dust threshold (546 satoshis)`)
        }

        tx.addOutput({
          satoshis: recipient.amount,
          lockingScript: Script.fromAddress(recipient.address)
        })

        // Optional: Add OP_RETURN memo if provided
        if (recipient.memo) {
          tx.addOutput({
            satoshis: 0,
            lockingScript: Script.fromASM(`OP_FALSE OP_RETURN ${Buffer.from(recipient.memo).toString('hex')}`)
          })
        }
      }

      // Calculate and add change output
      const actualFee = this.calculateActualFee(tx)
      const changeAmount = totalInput - totalPayment - actualFee

      if (changeAmount > 546) {
        tx.addOutput({
          satoshis: changeAmount,
          lockingScript: new P2PKH().lock(privateKey.toPublicKey().toHash())
        })
      } else if (changeAmount < 0) {
        throw new Error('Insufficient funds to cover payment and fees')
      }

      // Sign the transaction
      await tx.sign()

      return tx
    } catch (error) {
      throw new Error(`Batch payment processing failed: ${error.message}`)
    }
  }

  /**
   * Estimate transaction size in bytes
   */
  private estimateTransactionSize(numInputs: number, numOutputs: number): number {
    const baseSize = 10
    const inputSize = 148 // Average P2PKH input size
    const outputSize = 34 // Average P2PKH output size

    return baseSize + (numInputs * inputSize) + (numOutputs * outputSize)
  }

  /**
   * Select UTXOs to meet target amount
   */
  private selectUTXOs(
    utxos: Array<{ satoshis: number; [key: string]: any }>,
    targetAmount: number
  ): typeof utxos {
    // Sort UTXOs by size (largest first)
    const sorted = [...utxos].sort((a, b) => b.satoshis - a.satoshis)

    const selected: typeof utxos = []
    let total = 0

    for (const utxo of sorted) {
      selected.push(utxo)
      total += utxo.satoshis

      if (total >= targetAmount) {
        return selected
      }
    }

    throw new Error(`Insufficient funds: need ${targetAmount}, have ${total}`)
  }

  /**
   * Calculate actual transaction fee based on size
   */
  private calculateActualFee(tx: Transaction): number {
    const size = tx.toHex().length / 2 // Convert hex string length to bytes
    return Math.ceil(size * this.feePerByte)
  }
}

/**
 * Usage Example
 */
async function example() {
  const processor = new BatchPaymentProcessor()
  const privateKey = PrivateKey.fromWif('your-wif-key')

  const utxos = [
    {
      txid: 'abc123...',
      vout: 0,
      satoshis: 100000,
      script: new P2PKH().lock(privateKey.toPublicKey().toHash())
    },
    {
      txid: 'def456...',
      vout: 0,
      satoshis: 50000,
      script: new P2PKH().lock(privateKey.toPublicKey().toHash())
    }
  ]

  const recipients = [
    { address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', amount: 10000 },
    { address: '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2', amount: 20000, memo: 'Payment 1' },
    { address: '1JfbZRwdDHKZmuiZgYArJZhcuuzuw2HuMu', amount: 15000 }
  ]

  const tx = await processor.processBatchPayment(privateKey, utxos, recipients)

  console.log('Transaction ID:', tx.id('hex'))
  console.log('Total recipients:', recipients.length)
}

Batch UTXO Consolidation

Batch UTXO Distribution

Optimized Batch Processing

See Also

SDK Components:

Learning Paths:

Last updated