Sending Bitcoin

Overview

Sending Bitcoin on OP_NET is handled through the TransactionFactory.createBTCTransfer() method, which constructs a standard Bitcoin transfer without any smart contract interaction. This method manages the full transaction lifecycle internally selecting appropriate UTXOs, calculating fees based on the provided fee rate, and creating change outputs back to the sender.

Before calling createBTCTransfer(), use the UTXOManager to retrieve the available UTXOs for the target amount, ensuring the transaction has sufficient inputs to cover both the transfer value and the associated fees.

BTC Transfer Sequence Wallet UTXOManager TransactionFactory Network Get UTXOs > Wallet (dashed return) --> Available UTXOs createBTCTransfer() > Wallet (dashed return) --> Signed transaction Broadcast > Wallet (dashed return) --> Transaction ID Wallet UTXOManager TransactionFactory Network

Operations

Prerequisites

The following setup code is shared across all examples in this section. It initializes the provider, derives the wallet from a mnemonic, and creates the TransactionFactory instance used to build transactions.

typescript
Setup code
import { networks } from '@btc-vision/bitcoin';
import {
    AddressTypes,
    IFundingTransactionParameters,
    Mnemonic,
    MLDSASecurityLevel,
    TransactionFactory,
    OPNetLimitedProvider,
} from '@btc-vision/transaction';
import { JSONRpcProvider } from 'opnet';

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

// Create wallet from mnemonic
const mnemonic = new Mnemonic(
    'your twenty four word seed phrase goes here ...',
    '',
    network,
    MLDSASecurityLevel.LEVEL2,
);
const wallet = mnemonic.deriveOPWallet(AddressTypes.P2TR, 0);

// Create transaction factory
const factory = new TransactionFactory();
        

Simple Bitcoin Transfer

typescript
Basic send
async function sendBitcoin(
    recipient: string,
    amount: bigint,
): Promise<string> {
    // Get UTXOs for the transfer
    const utxos = await provider.utxoManager.getUTXOsForAmount({
        address: wallet.p2tr,
        amount: amount + 10000n,  // Add buffer for fees
        mergePendingUTXOs: true,
        filterSpentUTXOs: true,
        throwErrors: true,
    });

    // Build funding transaction parameters
    const params: IFundingTransactionParameters = {
        amount: amount,
        feeRate: 10,
        from: wallet.p2tr,
        to: recipient,
        utxos: utxos,
        signer: wallet.keypair,
        network: network,
        priorityFee: 0n,
        gasSatFee: 0n,
    };

    // Create the transfer
    const result = await factory.createBTCTransfer(params);

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

    if (!broadcast || broadcast.error) {
        throw new Error(`Broadcast failed: ${broadcast?.error}`);
    }

    // Track spent UTXOs for future transactions
    provider.utxoManager.spentUTXO(wallet.p2tr, result.inputUtxos, result.nextUTXOs);

    return broadcast.result;
}

// Usage
const txId = await sendBitcoin(
    'bcrt1p...recipient...',
    50000n,  // 50,000 satoshis
);
console.log('Transaction sent:', txId);

Splitting UTXOs

typescript
Split a large UTXO into multiple smaller outputs
import { BitcoinUtils } from 'opnet';

async function splitUTXOs(
    recipient: string,
    totalAmount: bigint,
    splitCount: number,
): Promise<string> {
    const utxos = await provider.utxoManager.getUTXOsForAmount({
        address: wallet.p2tr,
        amount: totalAmount + 50000n,  // Extra buffer for fees with many outputs
        mergePendingUTXOs: true,
        filterSpentUTXOs: true,
        throwErrors: true,
    });

    const params: IFundingTransactionParameters = {
        amount: totalAmount,
        feeRate: 1.5,
        from: wallet.p2tr,
        to: recipient,
        utxos: utxos,
        signer: wallet.keypair,
        network: network,
        priorityFee: 0n,
        gasSatFee: 0n,
        splitInputsInto: splitCount,  // Split into this many outputs
    };

    const result = await factory.createBTCTransfer(params);
    const broadcast = await limitedProvider.broadcastTransaction(result.tx, false);

    if (!broadcast) {
        throw new Error('Could not broadcast transaction');
    }

    return broadcast.result;
}

// Usage: Split 5 BTC into 500 UTXOs
const amount = BitcoinUtils.expandToDecimals(5, 8);  // 5 BTC in satoshis
const txId = await splitUTXOs(wallet.p2tr, amount - 10000n, 500);
console.log('Split transaction:', txId);
        

Consolidating UTXOs

typescript
Merge multiple small UTXOs into a single larger output
async function consolidateUTXOs(
    destinationAddress: string,
    maxUTXOs: number = 100,
): Promise<string> {
    // Fetch all available UTXOs
    const utxos = await provider.utxoManager.getUTXOs({
        address: wallet.p2tr,
        optimize: false,
        mergePendingUTXOs: false,
        filterSpentUTXOs: true,
    });

    // Limit number of UTXOs to consolidate
    const selectedUTXOs = utxos.slice(0, maxUTXOs);
    const total = selectedUTXOs.reduce((acc, utxo) => acc + utxo.value, 0n);

    console.log(`Consolidating ${selectedUTXOs.length} UTXOs with total value ${total} sats`);

    // Estimate fees - start with minimal fee and adjust if needed
    let fees = 1n;

    const params: IFundingTransactionParameters = {
        amount: total - fees,
        feeRate: 10,
        from: wallet.p2tr,
        to: destinationAddress,
        utxos: selectedUTXOs,
        signer: wallet.keypair,
        network: network,
        priorityFee: 0n,
        gasSatFee: 0n,
    };

    const result = await factory.createBTCTransfer(params);

    // Actual fees are calculated by the factory
    console.log(`Transaction size: ${result.tx.length / 2} bytes`);
    console.log(`Estimated fees: ${result.estimatedFees} sats`);

    const broadcast = await provider.sendRawTransaction(result.tx, false);

    if (!broadcast || broadcast.error) {
        throw new Error(`Broadcast failed: ${broadcast?.error}`);
    }

    return broadcast.result;
}

// Usage
const txId = await consolidateUTXOs(wallet.p2tr, 50);
console.log('Consolidation transaction:', txId);
        

Multi-Address UTXO Fetching

typescript
Fetch UTXOs from multiple addresses at once
import {
    FetchUTXOParamsMultiAddress,
    OPNetLimitedProvider,
    UTXO,
} from '@btc-vision/transaction';
import { BitcoinUtils } from 'opnet';

async function fetchMultiAddressUTXOs(): Promise<UTXO[]> {
    const limitedProvider = new OPNetLimitedProvider('https://regtest.opnet.org');

    const params: FetchUTXOParamsMultiAddress = {
        addresses: [wallet.p2tr, wallet.p2wpkh],
        minAmount: 10n,
        requestedAmount: BitcoinUtils.expandToDecimals(1, 8),  // 1 BTC
        optimized: true,
        usePendingUTXO: true,
    };

    const utxos = await limitedProvider.fetchUTXOMultiAddr(params);

    if (!utxos || !utxos.length) {
        throw new Error('No UTXOs found');
    }

    const total = utxos.reduce((acc, utxo) => acc + utxo.value, 0n);
    console.log(`Found ${utxos.length} UTXOs with total ${total} sats`);

    return utxos;
}
        

Fee Estimation

Get Network Fee Rates

typescript
Get network fee rates
async function getRecommendedFees(): Promise<{
    high: number;
    medium: number;
    low: number;
    conservative: number;
}> {
    const gasParams = await provider.gasParameters();

    return {
        high: gasParams.bitcoin.recommended.high,
        medium: gasParams.bitcoin.recommended.medium,
        low: gasParams.bitcoin.recommended.low,
        conservative: gasParams.bitcoin.conservative,
    };
}

// Usage
const fees = await getRecommendedFees();
console.log('Fee rates (sat/vB):');
console.log('  High:', fees.high);
console.log('  Medium:', fees.medium);
console.log('  Low:', fees.low);
console.log('  Conservative:', fees.conservative);
        

Use Automatic Fee Rate

typescript
Use automatic fee rate
const params: IFundingTransactionParameters = {
    amount: 50000n,
    feeRate: 0,  // Automatic fee rate
    from: wallet.p2tr,
    to: recipient,
    utxos: utxos,
    signer: wallet.keypair,
    network: network,
    priorityFee: 0n,
    gasSatFee: 0n,
};
        

Adding Notes to Transactions

typescript
Add an OP_RETURN note to your transaction
const params: IFundingTransactionParameters = {
    amount: 50000n,
    feeRate: 10,
    from: wallet.p2tr,
    to: recipient,
    utxos: utxos,
    signer: wallet.keypair,
    network: network,
    priorityFee: 0n,
    gasSatFee: 0n,
    note: 'Hello from OPNet!',  // String or Uint8Array
};
        

Broadcasting Transactions

Using JSONRpcProvider

typescript
Using JSONRpcProvider
const broadcast = await provider.sendRawTransaction(rawTxHex, false);

if (broadcast.error) {
    console.error('Broadcast failed:', broadcast.error);
} else {
    console.log('Transaction ID:', broadcast.result);
    console.log('Peers notified:', broadcast.peers);
}
        

Using OPNetLimitedProvider

typescript
Using OPNetLimitedProvider
const broadcast = await limitedProvider.broadcastTransaction(rawTxHex, false);

if (!broadcast) {
    throw new Error('Could not broadcast transaction');
}

console.log('Broadcast result:', broadcast);
        

Complete Example

typescript
Full transfer service
import { networks } from '@btc-vision/bitcoin';
import {
    IFundingTransactionParameters,
    TransactionFactory,
    Wallet,
} from '@btc-vision/transaction';
import { JSONRpcProvider } from 'opnet';

class BitcoinTransferService {
    private readonly factory = new TransactionFactory();

    constructor(
        private readonly provider: JSONRpcProvider,
        private readonly wallet: Wallet,
        private readonly network: typeof networks.bitcoin,
    ) {}

    async getBalance(): Promise<bigint> {
        return this.provider.getBalance(this.wallet.p2tr);
    }

    async send(
        recipient: string,
        amount: bigint,
        feeRate: number = 10,
    ): Promise<{ txId: string; fees: bigint }> {
        const utxos = await this.provider.utxoManager.getUTXOsForAmount({
            address: this.wallet.p2tr,
            amount: amount + 20000n,
            mergePendingUTXOs: true,
            filterSpentUTXOs: true,
            throwErrors: true,
        });

        const params: IFundingTransactionParameters = {
            amount: amount,
            feeRate: feeRate,
            from: this.wallet.p2tr,
            to: recipient,
            utxos: utxos,
            signer: this.wallet.keypair,
            network: this.network,
            priorityFee: 0n,
            gasSatFee: 0n,
        };

        const result = await this.factory.createBTCTransfer(params);
        const broadcast = await this.provider.sendRawTransaction(result.tx, false);

        if (!broadcast || broadcast.error) {
            throw new Error(`Broadcast failed: ${broadcast?.error}`);
        }

        // Update UTXO tracking
        this.provider.utxoManager.spentUTXO(
            this.wallet.p2tr,
            result.inputUtxos,
            result.nextUTXOs,
        );

        return {
            txId: broadcast.result,
            fees: result.estimatedFees,
        };
    }

    async split(
        amount: bigint,
        splitCount: number,
        feeRate: number = 10,
    ): Promise<string> {
        const utxos = await this.provider.utxoManager.getUTXOsForAmount({
            address: this.wallet.p2tr,
            amount: amount + 50000n,
            mergePendingUTXOs: true,
            filterSpentUTXOs: true,
            throwErrors: true,
        });

        const params: IFundingTransactionParameters = {
            amount: amount,
            feeRate: feeRate,
            from: this.wallet.p2tr,
            to: this.wallet.p2tr,
            utxos: utxos,
            signer: this.wallet.keypair,
            network: this.network,
            priorityFee: 0n,
            gasSatFee: 0n,
            splitInputsInto: splitCount,
        };

        const result = await this.factory.createBTCTransfer(params);
        const broadcast = await this.provider.sendRawTransaction(result.tx, false);

        if (!broadcast || broadcast.error) {
            throw new Error(`Broadcast failed: ${broadcast?.error}`);
        }

        return broadcast.result;
    }

    async consolidate(maxUTXOs: number = 100): Promise<string> {
        const utxos = await this.provider.utxoManager.getUTXOs({
            address: this.wallet.p2tr,
            optimize: false,
            mergePendingUTXOs: false,
            filterSpentUTXOs: true,
        });

        const selectedUTXOs = utxos.slice(0, maxUTXOs);
        const total = selectedUTXOs.reduce((acc, utxo) => acc + utxo.value, 0n);

        const params: IFundingTransactionParameters = {
            amount: total - 1000n,  // Reserve for fees
            feeRate: 10,
            from: this.wallet.p2tr,
            to: this.wallet.p2tr,
            utxos: selectedUTXOs,
            signer: this.wallet.keypair,
            network: this.network,
            priorityFee: 0n,
            gasSatFee: 0n,
        };

        const result = await this.factory.createBTCTransfer(params);
        const broadcast = await this.provider.sendRawTransaction(result.tx, false);

        if (!broadcast || broadcast.error) {
            throw new Error(`Broadcast failed: ${broadcast?.error}`);
        }

        return broadcast.result;
    }
}

// Usage
const network = networks.regtest;
const provider = new JSONRpcProvider({ url: 'https://regtest.opnet.org', network });
const mnemonic = new Mnemonic(
    'your twenty four word seed phrase goes here ...',
    '',
    network,
    MLDSASecurityLevel.LEVEL2,
);
const wallet = mnemonic.deriveUnisat(AddressTypes.P2TR, 0);

const service = new BitcoinTransferService(provider, wallet, network);

// Check balance
const balance = await service.getBalance();
console.log('Balance:', balance, 'sats');

// Send Bitcoin
const { txId, fees } = await service.send('bcrt1p...recipient...', 50000n);
console.log('Sent! TxID:', txId, 'Fees:', fees);

// Split UTXOs
const splitTxId = await service.split(1000000n, 10);
console.log('Split! TxID:', splitTxId);

// Consolidate UTXOs
const consolidateTxId = await service.consolidate(50);
console.log('Consolidated! TxID:', consolidateTxId);