Broadcasting Transactions

Overview

After building and signing a transaction, it must be broadcast to the network for inclusion in a block. The provider exposes methods for both single and batch transaction broadcasting, enabling efficient submission of one or multiple transactions. Each broadcast returns confirmation of acceptance and relevant transaction identifiers for tracking purposes.

How Transactions Get Broadcast From your wallet to the Bitcoin network You submit transactions one or more, as an ordered batch How many transactions? JUST 1 Simple Path Validate the transaction against the mempool. If it passes, send it out. Done MULTIPLE Try package mode? YES Package Relay Submit all transactions as a single atomic package. The network validates them together, including fees. Submission succeeded? YES Done NO Falls back gracefully tries the one-by-one approach NO One-by-one approach (or fallback from package failure) Validate Everything First Check every transaction against the mempool before sending any. Did they all pass? NO Rejected tells you what failed YES Send One by One Send each transaction in order. If one fails, stop immediately since later ones depend on it. Stopped early partial results returned repeats All sent Success Failure Fallback Flow Graceful retry Decision

Sending a Single Transaction

Using sendRawTransaction()

The sendRawTransaction() method broadcasts a signed transaction to the network. The method accepts a raw transaction as a hexadecimal string and a boolean indicating whether the transaction is a PSBT (Partially Signed Bitcoin Transaction).

The method returns a BroadcastedTransaction object containing the result of the broadcast operation, including the transaction identifier and any error information if the broadcast failed.

Method Signature

typescript
sendRawTransaction
async sendRawTransaction(
    tx: string,        // Raw transaction as hex string
    psbt: boolean      // Whether the transaction is a PSBT
): Promise<BroadcastedTransaction>
        

BroadcastedTransaction Result

The BroadcastedTransaction interface represents the result of a broadcast operation. The success field indicates whether the broadcast succeeded, while result contains the transaction ID on success. If the broadcast failed, error provides the failure reason. The optional peers field reports the number of network peers that received the transaction.

typescript
BroadcastedTransaction
interface BroadcastedTransaction {
    success: boolean;     // Whether broadcast succeeded
    result?: string;      // Transaction ID if successful
    error?: string;       // Error message if failed
    peers?: number;       // Number of peers that received the transaction
}

Basic Transaction Broadcasting

typescript
Basic transaction broadcasting
import { JSONRpcProvider } from 'opnet';
import { networks } from '@btc-vision/bitcoin';

const network = networks.regtest;
const provider = new JSONRpcProvider({ url: 'https://regtest.opnet.org', network });

// Raw signed transaction as hex string
const rawTx = '02000000000101ad897689f66c98daae5fdc3606235c1ad7...';

// Broadcast the transaction
const result = await provider.sendRawTransaction(rawTx, false);

if (result.success) {
    console.log('Transaction broadcast successfully!');
    console.log('TxID:', result.result);
    console.log('Peers:', result.peers);
} else {
    console.log('Broadcast failed:', result.error);
}

Basic Transaction Broadcasting with PSBT

typescript
PSBT transaction broadcasting
// For PSBT (Partially Signed Bitcoin Transactions)
const psbt = '70736274ff...'; // PSBT as hex

const result = await provider.sendRawTransaction(psbt, true);

if (result.success) {
    console.log('PSBT broadcast successful');
}

Sequential Broadcasting Strategy

When transaction order matters or when avoiding potential mempool conflicts, transactions can be broadcast sequentially with a small delay between each submission. This approach ensures each transaction is accepted before the next is sent, providing clearer error attribution if a broadcast fails.

typescript
Sequential broadcasting
async function broadcastSequentially(
    provider: JSONRpcProvider,
    transactions: string[]
): Promise<BroadcastedTransaction[]> {
    const results: BroadcastedTransaction[] = [];

    for (const tx of transactions) {
        const result = await provider.sendRawTransaction(tx, false);
        results.push(result);

        // Small delay between broadcasts
        await new Promise(r => setTimeout(r, 100));
    }

    return results;
}

Sending Transaction Package

Using sendRawTransactionPackage()

The sendRawTransactionPackage() method broadcasts multiple signed transactions to the network in a single request. The method accepts an array of raw transactions as hexadecimal strings, with a maximum of 25 transactions per batch. The optional isPackage parameter controls whether transactions are submitted atomically using the package submission mechanism, defaulting to true.

The method returns a BroadcastedTransactionPackage object containing the results for each transaction in the batch. Atomic package submission ensures that all transactions in the batch are either accepted or rejected together, which is useful for dependent transaction chains where later transactions rely on outputs from earlier ones.

Method Signature

typescript
sendRawTransactionPackage() signature
async sendRawTransactionPackage(
    txs: string[],          // Raw transactions as hex strings (max 25)
    isPackage?: boolean     // Use atomic submitpackage (default: true)
): Promise<BroadcastedTransactionPackage>
        

BroadcastedTransactionPackage Result

The BroadcastedTransactionPackage interface represents the result of a batch broadcast operation. The success field indicates overall success, while error contains any failure message. The testResults array provides validation results from mempool acceptance testing. When atomic submission is used, packageResult contains the package submission outcome; otherwise, sequentialResults provides individual results for each transaction. The fellBackToSequential flag indicates whether the method reverted to sequential broadcasting after atomic submission failed.

typescript
BroadcastedTransactionPackage
interface BroadcastedTransactionPackage {
    success: boolean;                              // Whether the overall broadcast succeeded
    error?: string;                                // Error message if failed
    testResults?: readonly TestMempoolAcceptResult[]; // From testmempoolaccept validation
    packageResult?: PackageResult;                 // From submitpackage (atomic path)
    sequentialResults?: readonly SequentialBroadcastTxResult[]; // Per-tx results (sequential path)
    fellBackToSequential?: boolean;                // True if submitpackage failed and fell back
}

interface SequentialBroadcastTxResult {
    txid: string;          // The txid of the transaction
    success: boolean;      // Whether the individual transaction was broadcast
    error?: string;        // Error message if this transaction failed
    peers?: number;        // Number of peers that received the transaction
}

interface TestMempoolAcceptResult {
    txid: string;
    wtxid: string;
    allowed?: boolean;
    vsize?: number;
    fees?: TestMempoolAcceptFees;
    packageError?: string;
    rejectReason?: string;
    rejectDetails?: string;
}

interface PackageResult {
    packageMsg: string;
    txResults: { [wtxid: string]: PackageTxResult };
    replacedTransactions?: readonly string[];
}

Atomic Package Broadcasting

Use sendRawTransactionPackage to broadcast an ordered array of raw transactions atomically via Bitcoin Core's submitpackage RPC. This is ideal for CPFP (Child-Pays-For-Parent) chains or any set of dependent transactions that must be accepted together.

typescript
Atomic package broadcasting
import { JSONRpcProvider, BroadcastedTransactionPackage } from 'opnet';
import { networks } from '@btc-vision/bitcoin';

const network = networks.regtest;
const provider = new JSONRpcProvider({ url: 'https://regtest.opnet.org', network });

const parentTx = '02000000000101...';
const childTx = '02000000000101...';

// Atomic package submission (uses submitpackage RPC)
const result: BroadcastedTransactionPackage = await provider.sendRawTransactionPackage(
    [parentTx, childTx],
    true,  // isPackage=true (default) for atomic submission
);

if (result.success) {
    console.log('Package broadcast successfully!');

    if (result.packageResult) {
        console.log('Package message:', result.packageResult.packageMsg);
        for (const [wtxid, txResult] of Object.entries(result.packageResult.txResults)) {
            console.log(`  ${wtxid}: txid=${txResult.txid}, vsize=${txResult.vsize}`);
        }
    }

    if (result.sequentialResults) {
        for (const seq of result.sequentialResults) {
            console.log(`${seq.txid}: success=${seq.success}, peers=${seq.peers}`);
        }
    }
} else {
    console.log('Package broadcast failed:', result.error);
}

Sequential Validated Broadcasting

Set isPackage to false to use validated sequential broadcast instead. This first validates all transactions with testmempoolaccept, then broadcasts each one individually.

typescript
Sequential validated broadcasting
// Sequential broadcast (testmempoolaccept + sendrawtransaction)
const result = await provider.sendRawTransactionPackage(
    [tx1, tx2, tx3],
    false,  // sequential validated broadcast
);

if (result.success) {
    if (result.testResults) {
        for (const test of result.testResults) {
            console.log(`${test.txid}: allowed=${test.allowed}, vsize=${test.vsize}`);
        }
    }

    if (result.sequentialResults) {
        for (const seq of result.sequentialResults) {
            console.log(`${seq.txid}: success=${seq.success}, peers=${seq.peers}`);
        }
    }
}

// Check if the node fell back to sequential from package
if (result.fellBackToSequential) {
    console.log('submitpackage failed, fell back to sequential broadcast');
}

Sending Multiple Transactions

Using sendRawTransactions()

The sendRawTransactions() method broadcasts multiple signed transactions to the network individually, processing each transaction in sequence. The method accepts an array of raw transactions as hexadecimal strings and broadcasts them one at a time.

The method returns an array of BroadcastedTransaction objects, one for each submitted transaction. Unlike sendRawTransactionPackage(), this method does not provide atomic submission guarantees, meaning some transactions may succeed while others fail. This approach is suitable when transactions are independent and do not require coordinated acceptance.

Method Signature

typescript
sendRawTransactions() signature
async sendRawTransactions(
    txs: string[]  // Array of raw transactions as hex strings
): Promise<BroadcastedTransaction[]>
        

Batch Broadcasting

typescript
Batch broadcasting
const rawTransactions = [
    '02000000000101...',  // Transaction 1
    '02000000000101...',  // Transaction 2
    '02000000000101...',  // Transaction 3
];

const results = await provider.sendRawTransactions(rawTransactions);

for (let i = 0; i < results.length; i++) {
    const result = results[i];
    if (result.success) {
        console.log(`TX ${i + 1} success: ${result.result}`);
    } else {
        console.log(`TX ${i + 1} failed: ${result.error}`);
    }
}

Parallel Broadcasting Strategie

For independent transactions that do not rely on each other, parallel broadcasting improves throughput by submitting multiple transactions concurrently. Processing transactions in batches balances speed with resource management, preventing network congestion while maximizing submission efficiency.

typescript
Parallel broadcasting
async function broadcastParallel(
    provider: JSONRpcProvider,
    transactions: string[],
    batchSize: number = 10
): Promise<BroadcastedTransaction[]> {
    const results: BroadcastedTransaction[] = [];

    // Process in batches
    for (let i = 0; i < transactions.length; i += batchSize) {
        const batch = transactions.slice(i, i + batchSize);
        const batchResults = await provider.sendRawTransactions(batch);
        results.push(...batchResults);
    }

    return results;
}

// Usage
const results = await broadcastParallel(provider, transactions, 5);
const successful = results.filter(r => r.success).length;
console.log(`Broadcast ${successful}/${transactions.length} successfully`);

Error Handling

Broadcast Retry Logic

Network conditions or temporary node issues may cause broadcast failures. Implementing retry logic with exponential backoff improves reliability by reattempting failed broadcasts. Certain errors such as duplicate transactions should not trigger retries, as the transaction has already been accepted. The following example demonstrates a retry wrapper that handles transient failures while avoiding unnecessary retries for permanent errors.

typescript
Broadcast retry logic
async function broadcastWithRetry(
    provider: JSONRpcProvider,
    rawTx: string,
    maxRetries: number = 3
): Promise<BroadcastedTransaction> {
    let lastError: string | undefined;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const result = await provider.sendRawTransaction(rawTx, false);

            if (result.success) {
                return result;
            }

            lastError = result.error;
            console.log(`Attempt ${attempt} failed: ${result.error}`);

            // Don't retry on certain errors
            if (
                result.error?.includes('already in block chain') ||
                result.error?.includes('already exists')
            ) {
                return result;
            }

        } catch (error: unknown) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            lastError = errorMessage;
            console.log(`Attempt ${attempt} error: ${errorMessage}`);
        }

        // Wait before retry
        if (attempt < maxRetries) {
            await new Promise(r => setTimeout(r, 2000 * attempt));
        }
    }

    return {
        success: false,
        error: lastError || 'Max retries exceeded',
    };
}

// Usage
const result = await broadcastWithRetry(provider, rawTx);
if (result.success) {
    console.log('Broadcast successful after retries');
}

Common Broadcast Errors

Broadcast operations may fail for various reasons related to transaction validity, fee levels, or network state. Parsing error messages enables applications to provide meaningful feedback and take appropriate action. The following table lists common error patterns and their descriptions:

Error Message Description
already in block chain Transaction has already been confirmed in a block.
already exists / txn-mempool-conflict Transaction is already present in the mempool.
insufficient fee / min relay fee not met Transaction fee is below the minimum required for relay.
bad-txns-inputs-spent One or more inputs have already been spent (double spend).
non-final Transaction locktime has not been reached.
dust One or more outputs are below the dust threshold.
typescript
Handle broadcast errors
function handleBroadcastError(error: string | undefined): string {
    if (!error) return 'Unknown error';

    if (error.includes('already in block chain')) {
        return 'Transaction already confirmed';
    }

    if (error.includes('already exists') || error.includes('txn-mempool-conflict')) {
        return 'Transaction already in mempool';
    }

    if (error.includes('insufficient fee') || error.includes('min relay fee not met')) {
        return 'Fee too low';
    }

    if (error.includes('bad-txns-inputs-spent')) {
        return 'Double spend detected';
    }

    if (error.includes('non-final')) {
        return 'Transaction is not final (locktime)';
    }

    if (error.includes('dust')) {
        return 'Output too small (dust)';
    }

    return error;
}

// Usage
const result = await provider.sendRawTransaction(rawTx, false);
if (!result.success) {
    const friendlyError = handleBroadcastError(result.error);
    console.log('Broadcast failed:', friendlyError);
}

Broadcast Confirmation

Verifying Broadcast Results

A successful broadcast response indicates the transaction was accepted by the connected node, but does not guarantee network-wide propagation. Verifying that the transaction is retrievable from the provider confirms it has been propagated and indexed. The following example polls for the transaction until it becomes available or a timeout is reached.

typescript
Verifying broadcast results
async function verifyBroadcast(
    provider: JSONRpcProvider,
    txHash: string,
    timeoutMs: number = 30000
): Promise<boolean> {
    const startTime = Date.now();

    while (Date.now() - startTime < timeoutMs) {
        try {
            const tx = await provider.getTransaction(txHash);
            if (tx) {
                return true;
            }
        } catch {
            // Transaction not found yet
        }

        await new Promise(r => setTimeout(r, 2000));
    }

    return false;
}

// Usage
const result = await provider.sendRawTransaction(rawTx, false);

if (result.success && result.result) {
    const verified = await verifyBroadcast(provider, result.result);
    console.log('Transaction verified:', verified);
}

Wait for Confirmation

For critical operations, waiting for block confirmation provides stronger guarantees than broadcast success alone. The following example broadcasts a transaction and polls until it reaches the desired confirmation depth or a timeout is reached. This pattern is useful for applications that need to ensure transaction finality before proceeding with dependent operations.

typescript
Wait for confirmation
async function broadcastAndConfirm(
    provider: JSONRpcProvider,
    rawTx: string,
    confirmations: number = 1,
    timeoutMs: number = 600000
): Promise<{
    txHash: string;
    confirmations: number;
    blockNumber?: bigint;
}> {
    // Broadcast
    const broadcastResult = await provider.sendRawTransaction(rawTx, false);

    if (!broadcastResult.success || !broadcastResult.result) {
        throw new Error(`Broadcast failed: ${broadcastResult.error}`);
    }

    const txHash = broadcastResult.result;
    const startTime = Date.now();

    // Wait for confirmations
    while (Date.now() - startTime < timeoutMs) {
        try {
            const tx = await provider.getTransaction(txHash);

            if (tx.blockNumber !== undefined) {
                const currentBlock = await provider.getBlockNumber();
                const confs = Number(currentBlock - tx.blockNumber) + 1;

                if (confs >= confirmations) {
                    return {
                        txHash,
                        confirmations: confs,
                        blockNumber: tx.blockNumber,
                    };
                }
            }
        } catch {
            // Transaction not found yet
        }

        await new Promise(r => setTimeout(r, 10000));
    }

    throw new Error('Timeout waiting for confirmation');
}

// Usage
try {
    const confirmed = await broadcastAndConfirm(provider, rawTx, 1);
    console.log('Confirmed in block:', confirmed.blockNumber);
} catch (error) {
    console.error('Failed to confirm:', error);
}

Complete Broadcast Service

typescript
Broadcast Service
class BroadcastService {
    constructor(private provider: JSONRpcProvider) {}

    async broadcast(
        rawTx: string,
        isPsbt: boolean = false
    ): Promise<BroadcastedTransaction> {
        return this.provider.sendRawTransaction(rawTx, isPsbt);
    }

    async broadcastBatch(
        transactions: string[]
    ): Promise<BroadcastedTransaction[]> {
        return this.provider.sendRawTransactions(transactions);
    }

    async broadcastWithRetry(
        rawTx: string,
        maxRetries: number = 3
    ): Promise<BroadcastedTransaction> {
        for (let attempt = 1; attempt <= maxRetries; attempt++) {
            const result = await this.broadcast(rawTx);

            if (result.success) {
                return result;
            }

            // Don't retry on permanent failures
            if (this.isPermanentFailure(result.error)) {
                return result;
            }

            if (attempt < maxRetries) {
                await new Promise(r => setTimeout(r, 2000 * attempt));
            }
        }

        return { success: false, error: 'Max retries exceeded' };
    }

    async broadcastAndWait(
        rawTx: string,
        timeoutMs: number = 60000
    ): Promise<{
        broadcast: BroadcastedTransaction;
        confirmed: boolean;
    }> {
        const broadcast = await this.broadcast(rawTx);

        if (!broadcast.success || !broadcast.result) {
            return { broadcast, confirmed: false };
        }

        const confirmed = await this.waitForTransaction(
            broadcast.result,
            timeoutMs
        );

        return { broadcast, confirmed };
    }

    async waitForTransaction(
        txHash: string,
        timeoutMs: number = 60000
    ): Promise<boolean> {
        const startTime = Date.now();

        while (Date.now() - startTime < timeoutMs) {
            try {
                const tx = await this.provider.getTransaction(txHash);
                if (tx) return true;
            } catch {
                // Not found yet
            }

            await new Promise(r => setTimeout(r, 3000));
        }

        return false;
    }

    private isPermanentFailure(error: string | undefined): boolean {
        if (!error) return false;

        const permanentErrors = [
            'already in block chain',
            'already exists',
            'bad-txns-inputs-spent',
            'invalid',
        ];

        return permanentErrors.some(e => error.includes(e));
    }
}

// Usage
const broadcastService = new BroadcastService(provider);

// Simple broadcast
const result = await broadcastService.broadcast(rawTx);
console.log('Success:', result.success);

// Broadcast with retry
const retryResult = await broadcastService.broadcastWithRetry(rawTx);
console.log('Success with retry:', retryResult.success);

// Broadcast and wait
const { broadcast, confirmed } = await broadcastService.broadcastAndWait(rawTx);
console.log('Broadcast:', broadcast.success, 'Confirmed:', confirmed);