Transaction Broadcasting

Complete examples for broadcasting single, independent transactions to the BSV network using the SDK's built-in broadcasting functionality.

Overview

Broadcasting transactions is the final step in getting your transactions confirmed on the BSV blockchain. This guide covers simple transaction broadcasting using tx.broadcast() for single, independent transactions.

For transaction chains: See Broadcast Arc for BEEF bundle broadcasting.

Related SDK Components:

When to Use This Approach

Use tx.broadcast() when:

  • Broadcasting a single transaction

  • Transaction doesn't depend on unconfirmed parents

  • You want simple, straightforward broadcasting

Do NOT use for transaction chains - use BEEF broadcasting instead (see Broadcast Arc)

Basic Transaction Broadcasting

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

/**
 * Basic Transaction Broadcaster
 *
 * Broadcast single, independent transactions using SDK's built-in broadcast
 */
class TransactionBroadcaster {
  private arc?: ARC

  constructor(arcUrl?: string, apiKey?: string) {
    // Optional: Configure specific ARC instance
    // If not provided, tx.broadcast() will use default broadcaster
    if (arcUrl) {
      this.arc = new ARC(arcUrl, { apiKey })
    }
  }

  /**
   * Broadcast a single transaction
   * Uses SDK's tx.broadcast() method
   */
  async broadcastTransaction(tx: Transaction): Promise<BroadcastResult> {
    try {
      console.log('Broadcasting transaction...')
      console.log('Transaction ID:', tx.id('hex'))
      console.log('Size:', tx.toHex().length / 2, 'bytes')

      // Broadcast using SDK's built-in broadcast method
      // Pass ARC instance if configured, otherwise uses default
      const response = this.arc
        ? await tx.broadcast(this.arc)
        : await tx.broadcast()

      const result: BroadcastResult = {
        txid: response.txid,
        status: response.status,
        timestamp: Date.now(),
        blockHash: response.blockHash,
        blockHeight: response.blockHeight
      }

      console.log('Transaction broadcast successful')
      console.log('Status:', result.status)

      return result
    } catch (error) {
      throw new Error(`Broadcast failed: ${error.message}`)
    }
  }

  /**
   * Broadcast and wait for confirmation
   */
  async broadcastAndWait(
    tx: Transaction,
    maxWaitTime: number = 60000 // 60 seconds default
  ): Promise<BroadcastResult> {
    try {
      const result = await this.broadcastTransaction(tx)

      console.log('Waiting for confirmation...')

      const startTime = Date.now()

      while (Date.now() - startTime < maxWaitTime) {
        const status = await this.checkTransactionStatus(result.txid)

        if (status.status === 'MINED') {
          console.log('Transaction confirmed')
          console.log('Block height:', status.blockHeight)
          return status
        }

        // Wait 2 seconds before next check
        await new Promise(resolve => setTimeout(resolve, 2000))
      }

      throw new Error('Transaction confirmation timeout')
    } catch (error) {
      throw new Error(`Broadcast and wait failed: ${error.message}`)
    }
  }

  /**
   * Check transaction status
   */
  async checkTransactionStatus(txid: string): Promise<BroadcastResult> {
    try {
      if (!this.arc) {
        throw new Error('ARC instance required for status checks. Provide arcUrl in constructor.')
      }

      const status = await this.arc.getTransactionStatus(txid)

      return {
        txid,
        status: status.status,
        timestamp: Date.now(),
        blockHash: status.blockHash,
        blockHeight: status.blockHeight
      }
    } catch (error) {
      throw new Error(`Status check failed: ${error.message}`)
    }
  }

  /**
   * Broadcast multiple transactions
   */
  async broadcastBatch(
    transactions: Transaction[]
  ): Promise<BroadcastResult[]> {
    try {
      console.log('Broadcasting batch of', transactions.length, 'transactions')

      const results: BroadcastResult[] = []

      for (const tx of transactions) {
        try {
          const result = await this.broadcastTransaction(tx)
          results.push(result)
        } catch (error) {
          console.error('Failed to broadcast tx:', tx.id('hex'), error.message)
          results.push({
            txid: tx.id('hex'),
            status: 'FAILED',
            timestamp: Date.now(),
            error: error.message
          })
        }
      }

      console.log('Batch broadcast completed')
      console.log('Successful:', results.filter(r => r.status !== 'FAILED').length)
      console.log('Failed:', results.filter(r => r.status === 'FAILED').length)

      return results
    } catch (error) {
      throw new Error(`Batch broadcast failed: ${error.message}`)
    }
  }
}

interface BroadcastResult {
  txid: string
  status: string
  timestamp: number
  blockHash?: string
  blockHeight?: number
  error?: string
}

/**
 * Usage Example
 */
async function basicBroadcastExample() {
  const broadcaster = new TransactionBroadcaster()
  const privateKey = PrivateKey.fromRandom()

  // Create transaction
  const tx = new Transaction()

  tx.addInput({
    sourceTXID: 'source-tx...',
    sourceOutputIndex: 0,
    unlockingScriptTemplate: new P2PKH().unlock(privateKey),
    sequence: 0xffffffff
  })

  tx.addOutput({
    satoshis: 50000,
    lockingScript: Script.fromAddress('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa')
  })

  await tx.sign()

  // Broadcast
  const result = await broadcaster.broadcastTransaction(tx)
  console.log('Result:', result)

  // Or broadcast and wait for confirmation
  const confirmedResult = await broadcaster.broadcastAndWait(tx, 30000)
  console.log('Confirmed:', confirmedResult)
}

Advanced Broadcasting with Retry Logic

import { Transaction, ARC } from '@bsv/sdk'

/**
 * Advanced Broadcaster with Retry Logic
 *
 * Handles broadcast failures with automatic retries
 */
class AdvancedBroadcaster {
  private arcUrls: string[]
  private currentArcIndex: number = 0

  constructor(arcUrls: string[] = [
    'https://arc.taal.com',
    'https://arc.gorillapool.io'
  ]) {
    this.arcUrls = arcUrls
  }

  /**
   * Broadcast with automatic retry and failover
   */
  async broadcastWithRetry(
    tx: Transaction,
    maxRetries: number = 3
  ): Promise<BroadcastResult> {
    let lastError: Error | null = null

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        console.log(`Broadcast attempt ${attempt}/${maxRetries}`)

        const arcUrl = this.arcUrls[this.currentArcIndex]
        const arc = new ARC(arcUrl)

        console.log('Using ARC:', arcUrl)

        const response = await arc.broadcastTransaction(tx)

        console.log('Broadcast successful')

        return {
          txid: response.txid,
          status: response.txStatus,
          timestamp: Date.now(),
          arcUrl,
          attempts: attempt
        }
      } catch (error) {
        lastError = error
        console.error(`Attempt ${attempt} failed:`, error.message)

        // Try next ARC endpoint
        this.currentArcIndex = (this.currentArcIndex + 1) % this.arcUrls.length

        if (attempt < maxRetries) {
          // Exponential backoff
          const waitTime = Math.min(1000 * Math.pow(2, attempt - 1), 10000)
          console.log(`Waiting ${waitTime}ms before retry...`)
          await new Promise(resolve => setTimeout(resolve, waitTime))
        }
      }
    }

    throw new Error(`Broadcast failed after ${maxRetries} attempts: ${lastError?.message}`)
  }

  /**
   * Broadcast to multiple ARC endpoints simultaneously
   */
  async broadcastMultipleArcs(tx: Transaction): Promise<BroadcastResult[]> {
    console.log('Broadcasting to', this.arcUrls.length, 'ARC endpoints')

    const promises = this.arcUrls.map(async (arcUrl) => {
      try {
        const arc = new ARC(arcUrl)
        const response = await arc.broadcastTransaction(tx)

        return {
          txid: response.txid,
          status: response.txStatus,
          timestamp: Date.now(),
          arcUrl,
          success: true
        }
      } catch (error) {
        return {
          txid: tx.id('hex'),
          status: 'FAILED',
          timestamp: Date.now(),
          arcUrl,
          success: false,
          error: error.message
        }
      }
    })

    const results = await Promise.all(promises)

    const successful = results.filter(r => r.success).length
    console.log(`Broadcast to ${successful}/${this.arcUrls.length} endpoints successful`)

    return results
  }

  /**
   * Broadcast with validation
   */
  async broadcastWithValidation(tx: Transaction): Promise<BroadcastResult> {
    try {
      // Validate transaction before broadcast
      const validation = this.validateTransaction(tx)

      if (!validation.valid) {
        throw new Error(`Transaction validation failed: ${validation.errors.join(', ')}`)
      }

      console.log('Transaction validation passed')
      console.log('Inputs:', tx.inputs.length)
      console.log('Outputs:', tx.outputs.length)
      console.log('Size:', tx.toHex().length / 2, 'bytes')

      // Broadcast with retry
      return await this.broadcastWithRetry(tx)
    } catch (error) {
      throw new Error(`Validated broadcast failed: ${error.message}`)
    }
  }

  /**
   * Validate transaction structure
   */
  private validateTransaction(tx: Transaction): {
    valid: boolean
    errors: string[]
  } {
    const errors: string[] = []

    // Check inputs
    if (tx.inputs.length === 0) {
      errors.push('Transaction has no inputs')
    }

    // Check outputs
    if (tx.outputs.length === 0) {
      errors.push('Transaction has no outputs')
    }

    // Check output amounts
    for (let i = 0; i < tx.outputs.length; i++) {
      const output = tx.outputs[i]
      if (output.satoshis < 0) {
        errors.push(`Output ${i} has negative amount`)
      }
      if (output.satoshis > 0 && output.satoshis < 546) {
        errors.push(`Output ${i} below dust threshold (546 sats)`)
      }
    }

    // Check signatures
    for (let i = 0; i < tx.inputs.length; i++) {
      const input = tx.inputs[i]
      if (!input.unlockingScript || input.unlockingScript.chunks.length === 0) {
        errors.push(`Input ${i} has no unlocking script`)
      }
    }

    return {
      valid: errors.length === 0,
      errors
    }
  }
}

interface BroadcastResult {
  txid: string
  status: string
  timestamp: number
  arcUrl?: string
  attempts?: number
  success?: boolean
  error?: string
  blockHash?: string
  blockHeight?: number
}

/**
 * Usage Example
 */
async function advancedBroadcastExample() {
  const broadcaster = new AdvancedBroadcaster([
    'https://arc.taal.com',
    'https://arc.gorillapool.io'
  ])

  const privateKey = PrivateKey.fromRandom()

  // Create transaction
  const tx = new Transaction()
  // ... add inputs and outputs ...
  await tx.sign()

  // Broadcast with retry
  try {
    const result = await broadcaster.broadcastWithRetry(tx, 5)
    console.log('Broadcast successful:', result)
  } catch (error) {
    console.error('All broadcast attempts failed:', error.message)
  }

  // Or broadcast to multiple ARCs
  const results = await broadcaster.broadcastMultipleArcs(tx)
  console.log('Multi-ARC results:', results)
}

Transaction Monitoring

import { Transaction, ARC } from '@bsv/sdk'

/**
 * Transaction Monitor
 *
 * Monitor transaction status and confirmations
 */
class TransactionMonitor {
  private arc: ARC
  private monitoredTxs: Map<string, MonitoredTransaction> = new Map()

  constructor(arcUrl: string = 'https://arc.taal.com') {
    this.arc = new ARC(arcUrl)
  }

  /**
   * Start monitoring a transaction
   */
  async monitorTransaction(
    txid: string,
    callback: (status: TransactionStatus) => void
  ): Promise<void> {
    const monitored: MonitoredTransaction = {
      txid,
      startTime: Date.now(),
      callback,
      status: 'PENDING',
      checkCount: 0
    }

    this.monitoredTxs.set(txid, monitored)

    console.log('Started monitoring transaction:', txid)

    // Start monitoring loop
    this.monitorLoop(txid)
  }

  /**
   * Monitor loop for a transaction
   */
  private async monitorLoop(txid: string): Promise<void> {
    const monitored = this.monitoredTxs.get(txid)
    if (!monitored) return

    try {
      monitored.checkCount++

      const status = await this.arc.getTransactionStatus(txid)

      const txStatus: TransactionStatus = {
        txid,
        status: status.txStatus,
        timestamp: Date.now(),
        blockHash: status.blockHash,
        blockHeight: status.blockHeight,
        confirmations: status.confirmations || 0
      }

      console.log(`Check ${monitored.checkCount}: ${txid} - ${txStatus.status}`)

      // Call callback
      monitored.callback(txStatus)

      // Update monitored transaction
      monitored.status = txStatus.status
      monitored.lastCheck = Date.now()

      // Stop monitoring if confirmed
      if (txStatus.status === 'MINED' && txStatus.confirmations >= 6) {
        console.log('Transaction confirmed with 6 blocks:', txid)
        this.monitoredTxs.delete(txid)
        return
      }

      // Continue monitoring
      setTimeout(() => this.monitorLoop(txid), 5000) // Check every 5 seconds
    } catch (error) {
      console.error('Monitor error:', error.message)

      // Retry after delay
      setTimeout(() => this.monitorLoop(txid), 10000)
    }
  }

  /**
   * Stop monitoring a transaction
   */
  stopMonitoring(txid: string): void {
    this.monitoredTxs.delete(txid)
    console.log('Stopped monitoring:', txid)
  }

  /**
   * Get monitored transactions
   */
  getMonitoredTransactions(): MonitoredTransaction[] {
    return Array.from(this.monitoredTxs.values())
  }

  /**
   * Wait for confirmations
   */
  async waitForConfirmations(
    txid: string,
    requiredConfirmations: number = 6,
    timeout: number = 300000 // 5 minutes
  ): Promise<TransactionStatus> {
    return new Promise((resolve, reject) => {
      const startTime = Date.now()

      const checkStatus = async () => {
        try {
          if (Date.now() - startTime > timeout) {
            reject(new Error('Timeout waiting for confirmations'))
            return
          }

          const status = await this.arc.getTransactionStatus(txid)

          const confirmations = status.confirmations || 0

          console.log(`${txid}: ${confirmations}/${requiredConfirmations} confirmations`)

          if (confirmations >= requiredConfirmations) {
            resolve({
              txid,
              status: status.txStatus,
              timestamp: Date.now(),
              blockHash: status.blockHash,
              blockHeight: status.blockHeight,
              confirmations
            })
          } else {
            setTimeout(checkStatus, 5000) // Check every 5 seconds
          }
        } catch (error) {
          reject(error)
        }
      }

      checkStatus()
    })
  }
}

interface MonitoredTransaction {
  txid: string
  startTime: number
  callback: (status: TransactionStatus) => void
  status: string
  checkCount: number
  lastCheck?: number
}

interface TransactionStatus {
  txid: string
  status: string
  timestamp: number
  blockHash?: string
  blockHeight?: number
  confirmations: number
}

/**
 * Usage Example
 */
async function monitoringExample() {
  const monitor = new TransactionMonitor()

  const txid = 'your-transaction-id...'

  // Start monitoring with callback
  await monitor.monitorTransaction(txid, (status) => {
    console.log('Status update:', status)

    if (status.status === 'MINED') {
      console.log('Transaction mined in block:', status.blockHeight)
    }
  })

  // Or wait for specific confirmations
  try {
    const status = await monitor.waitForConfirmations(txid, 6)
    console.log('Transaction confirmed:', status)
  } catch (error) {
    console.error('Confirmation timeout:', error.message)
  }

  // Get all monitored transactions
  const monitored = monitor.getMonitoredTransactions()
  console.log('Monitoring', monitored.length, 'transactions')
}

BEEF Format Broadcasting

import { Transaction, ARC, BEEF } from '@bsv/sdk'

/**
 * BEEF Broadcaster
 *
 * Broadcast transactions using BEEF format for efficiency
 */
class BEEFBroadcaster {
  private arc: ARC

  constructor(arcUrl: string = 'https://arc.taal.com') {
    this.arc = new ARC(arcUrl)
  }

  /**
   * Broadcast transaction chain using BEEF
   */
  async broadcastBEEF(
    transactions: Transaction[]
  ): Promise<BroadcastResult[]> {
    try {
      console.log('Creating BEEF from', transactions.length, 'transactions')

      // Create BEEF format
      const beef = BEEF.fromTransactions(transactions)
      const beefHex = beef.toHex()

      console.log('BEEF size:', beefHex.length / 2, 'bytes')
      console.log('Broadcasting BEEF...')

      // Broadcast BEEF
      const response = await this.arc.broadcastBEEF(beefHex)

      const results: BroadcastResult[] = transactions.map(tx => ({
        txid: tx.id('hex'),
        status: 'BROADCASTED',
        timestamp: Date.now(),
        format: 'BEEF'
      }))

      console.log('BEEF broadcast successful')

      return results
    } catch (error) {
      throw new Error(`BEEF broadcast failed: ${error.message}`)
    }
  }

  /**
   * Broadcast transaction with merkle proofs
   */
  async broadcastWithProofs(
    tx: Transaction,
    proofs: MerkleProof[]
  ): Promise<BroadcastResult> {
    try {
      // Create BEEF with proofs
      const beef = new BEEF()
      beef.addTransaction(tx)

      for (const proof of proofs) {
        beef.addMerkleProof(proof)
      }

      const beefHex = beef.toHex()

      console.log('Broadcasting transaction with proofs')
      console.log('BEEF size:', beefHex.length / 2, 'bytes')

      const response = await this.arc.broadcastBEEF(beefHex)

      return {
        txid: tx.id('hex'),
        status: 'BROADCASTED',
        timestamp: Date.now(),
        format: 'BEEF',
        proofs: proofs.length
      }
    } catch (error) {
      throw new Error(`Broadcast with proofs failed: ${error.message}`)
    }
  }
}

interface BroadcastResult {
  txid: string
  status: string
  timestamp: number
  format?: string
  proofs?: number
}

interface MerkleProof {
  txid: string
  blockHeight: number
  merkleRoot: string
  nodes: string[]
}

/**
 * Usage Example
 */
async function beefBroadcastExample() {
  const broadcaster = new BEEFBroadcaster()

  // Create transaction chain
  const transactions: Transaction[] = []
  // ... create linked transactions ...

  // Broadcast as BEEF
  const results = await broadcaster.broadcastBEEF(transactions)
  console.log('BEEF broadcast results:', results)
}

See Also

SDK Components:

Learning Paths:

Last updated