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.
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
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.
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
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
// 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.
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
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.
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.
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.
// 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
async sendRawTransactions(
txs: string[] // Array of raw transactions as hex strings
): Promise<BroadcastedTransaction[]>
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.
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.
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. |
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.
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.
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
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);