Fetching and Tracking UTXOs

Overview

UTXOs are the fundamental building blocks of Bitcoin transactions: every transfer consumes existing UTXOs as inputs and creates new ones as outputs.

The UTXOsManager class provides comprehensive UTXO management, including fetching available UTXOs for a given address, selecting UTXOs that meet a target amount, filtering out spent or pending outputs, and tracking UTXO state across sequential operations. Proper UTXO management is essential for avoiding double-spend errors, minimizing transaction fees, and ensuring reliable transaction construction.

UTXO Lifecycle Management Address UTXOsManager Fetch UTXOs Filter Confirmed UTXOs Pending UTXOs Transaction Building Spent Tracking

Accessing UTXOs Manager

The UTXOsManager is accessed through the provider's utxoManager property. You do not instantiate it directly, the provider creates and manages the instance internally, ensuring it shares the same network configuration and connection.

typescript
How to access the UTXOs manager
import { JSONRpcProvider } from 'opnet';
import { networks } from '@btc-vision/bitcoin';

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

// Access the UTXOs manager
const utxoManager = provider.utxoManager;

        

Fetching UTXOs

To fetch UTXOs for an address, call getUTXOs() on the provider's utxoManager and pass a RequestUTXOsParams object with the target address and desired filtering options.

The RequestUTXOsParams interface provides options to control how UTXOs are selected and filtered before being returned. The interface is defined as follows:

typescript
RequestUTXOsParams Reference
interface RequestUTXOsParams {
    address: string;              // Required: address to fetch UTXOs for
    optimize?: boolean;           // Default: true - sort by value
    mergePendingUTXOs?: boolean;  // Default: true - include unconfirmed
    filterSpentUTXOs?: boolean;   // Default: true - exclude known spent
    olderThan?: bigint;           // Only UTXOs older than this block
    isCSV?: boolean;              // Default: false - CSV (timelock) UTXOs
}
        

Basic UTXO Fetch

typescript
Basic fetch for an address
const utxos = await utxoManager.getUTXOs({
    address: wallet.p2tr,
});

console.log('UTXOs found:', utxos.length);
for (const utxo of utxos) {
    console.log(`  ${utxo.transactionId}:${utxo.outputIndex} - ${utxo.value} sats`);
}
        

Fetch with Filters

typescript
Advanced fetch with filters
// Get only confirmed UTXOs
const confirmedOnly = await utxoManager.getUTXOs({
    address: wallet.p2tr,
    mergePendingUTXOs: false,
});

// Get UTXOs optimized by value (largest first)
const optimized = await utxoManager.getUTXOs({
    address: wallet.p2tr,
    optimize: true,
});

// Include spent UTXOs (for debugging)
const allUtxos = await utxoManager.getUTXOs({
    address: wallet.p2tr,
    filterSpentUTXOs: false,
});
        

Fetching UTXOs for Specific Amount

To fetch UTXOs that cover a specific amount, call getUTXOsForAmount() on the provider's utxoManager and pass a RequestUTXOsParamsWithAmount object with the target address and the required satoshi amount.

The RequestUTXOsParamsWithAmount interface extends RequestUTXOsParams with additional properties, the most important being amount which specifies the minimum satoshi value the selected UTXOs must collectively satisfy.

typescript
RequestUTXOsParamsWithAmount Reference
interface RequestUTXOsParamsWithAmount extends RequestUTXOsParams {
    amount: bigint;                    // Required: amount needed
    throwErrors?: boolean;             // Default: false - throw if insufficient
    csvAddress?: string;               // Optional: CSV address for priority
    maxUTXOs?: number;                 // Default: 5000 - max UTXOs to select
    throwIfUTXOsLimitReached?: boolean; // Throw if limit reached
}
        

Get Enough UTXOs for Amount

typescript
Get UTXOs for an amount
const utxos = await utxoManager.getUTXOsForAmount({
    address: wallet.p2tr,
    amount: 100000n,  // 100,000 satoshis needed
});

// Calculate total value
const total = utxos.reduce((sum, utxo) => sum + utxo.value, 0n);
console.log(`Selected ${utxos.length} UTXOs totaling ${total} sats`);
        

With Error Handling

typescript
Get UTXOs for an amount with error handling
async function getRequiredUTXOs(
    utxoManager: UTXOsManager,
    address: string,
    amount: bigint
): Promise<UTXO[]> {
    try {
        const utxos = await utxoManager.getUTXOsForAmount({
            address,
            amount,
            throwErrors: true,
        });

        return utxos;
    } catch (error) {
        if (error instanceof Error && error.message.includes('Insufficient')) {
            throw new Error(`Not enough funds. Need ${amount} sats`);
        }
        throw error;
    }
}
        

Tracking Spent UTXOs

After broadcasting a transaction, call spentUTXO() to mark the consumed UTXOs as spent in the manager's local state. This prevents subsequent calls to getUTXOs() or getUTXOsForAmount() from returning UTXOs that have already been used but are not yet confirmed on-chain. Failing to track spent UTXOs between sequential transactions will result in double-spend rejections.

typescript
Mark UTXOs as spent
import { CallResult, TransactionParameters, UTXOsManager } from 'opnet';

async function sendTransactionWithTracking<T extends Record<string, unknown>>(
    simulation: CallResult<T>,
    params: TransactionParameters,
    utxoManager: UTXOsManager
): Promise<string> {
    const receipt = await simulation.sendTransaction(params);

    // Track UTXO state
    if (params.utxos) {
        utxoManager.spentUTXO(
            params.refundTo,
            params.utxos,
            receipt.newUTXOs
        );
    }

    return receipt.transactionId;
}
        

Pending UTXOs

To retrieve UTXOs from transactions that have been broadcast but not yet confirmed, call getPendingUTXOs(). This returns UTXOs currently sitting in the mempool, which can be useful for tracking incoming payments before they are mined or for building transactions that chain off unconfirmed outputs.

Get Pending (Unconfirmed) UTXOs

typescript
Get pending UTXOs
const pendingUtxos = utxoManager.getPendingUTXOs(wallet.p2tr);

console.log('Pending UTXOs:', pendingUtxos.length);
for (const utxo of pendingUtxos) {
    console.log(`  Pending: ${utxo.transactionId}:${utxo.outputIndex}`);
}
        

Working with Pending UTXOs

typescript
Waiting for UTXOs confirmation
async function waitForConfirmation(
    provider: JSONRpcProvider,
    address: string,
    maxWaitMs: number = 60000
): Promise<void> {
    const startTime = Date.now();

    while (Date.now() - startTime < maxWaitMs) {
        const pending = provider.utxoManager.getPendingUTXOs(address);

        if (pending.length === 0) {
            console.log('All UTXOs confirmed');
            return;
        }

        console.log(`Waiting for ${pending.length} pending UTXOs...`);
        await new Promise((resolve) => setTimeout(resolve, 5000));
    }

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

Mempool Chain Limit

OP_NET enforces a mempool chain limit of 25 unconfirmed transaction descendants. If you chain more than 25 transactions without waiting for confirmations, subsequent broadcasts will be rejected by the network.

typescript
Too many unconfirmed transactions
// This will throw if limit exceeded
try {
    utxoManager.spentUTXO(address, spentUtxos, newUtxos);
} catch (error) {
    if (error instanceof Error && error.message.includes('too-long-mempool-chain')) {
        console.error('Too many unconfirmed transactions. Wait for confirmation.');
    }
}
        

Multiple Address UTXO Fetch

To fetch UTXOs for multiple addresses in a single request, use getMultipleUTXOs(). This reduces network round-trips compared to calling getUTXOs() individually for each address, making it well suited for applications managing multiple wallets or monitoring several addresses simultaneously.

typescript
Batch UTXO Query
const result = await utxoManager.getMultipleUTXOs({
    requests: [
        { address: 'bcrt1p...address1...' },
        { address: 'bcrt1p...address2...' },
        { address: 'bcrt1p...address3...' },
    ],
    mergePendingUTXOs: true,
    filterSpentUTXOs: true,
});

// result is Record<string, UTXOs>
for (const [address, utxos] of Object.entries(result)) {
    const total = utxos.reduce((sum, u) => sum + u.value, 0n);
    console.log(`${address}: ${utxos.length} UTXOs, ${total} sats`);
}
        

Cleaning UTXO State

To clear the UTXOsManager's internal state, call clean() with either no arguments to reset all tracked addresses, or a specific address to reset only that address. This forces subsequent UTXO queries to fetch fresh data from the network, which is useful after external wallet activity or when switching between accounts.

typescript
Reset Cache
// Reset single address
utxoManager.clean(wallet.p2tr);

// Reset all addresses
utxoManager.clean();
        

Call clean() after transactions have been confirmed on-chain to discard stale cached data and allow subsequent queries to reflect the current blockchain state.

typescript
Clean after transaction confirmation
// After transaction confirmation
async function onTransactionConfirmed(
    provider: JSONRpcProvider,
    address: string
): Promise<void> {
    // Clean cached state
    provider.utxoManager.clean(address);

    // Fetch fresh UTXOs
    const utxos = await provider.utxoManager.getUTXOs({
        address,
        mergePendingUTXOs: false,  // Only confirmed
    });

    console.log('Confirmed UTXOs:', utxos.length);
}
        

Basic UTXO Service Example

typescript
UTXO Service Example
class UTXOService {
    private provider: JSONRpcProvider;

    constructor(provider: JSONRpcProvider) {
        this.provider = provider;
    }

    get manager(): UTXOsManager {
        return this.provider.utxoManager;
    }

    async getBalance(address: string): Promise<bigint> {
        const utxos = await this.manager.getUTXOs({ address });
        return utxos.reduce((sum, utxo) => sum + utxo.value, 0n);
    }

    async getSpendableUTXOs(
        address: string,
        amount: bigint
    ): Promise<UTXO[]> {
        return this.manager.getUTXOsForAmount({
            address,
            amount,
            optimize: true,
            throwErrors: true,
        });
    }

    async countUTXOs(address: string): Promise<{
        confirmed: number;
        pending: number;
    }> {
        const all = await this.manager.getUTXOs({
            address,
            mergePendingUTXOs: true,
        });

        const confirmed = await this.manager.getUTXOs({
            address,
            mergePendingUTXOs: false,
        });

        return {
            confirmed: confirmed.length,
            pending: all.length - confirmed.length,
        };
    }

    markSpent(address: string, spent: UTXO[], newUTXOs: UTXO[]): void {
        this.manager.spentUTXO(address, spent, newUTXOs);
    }

    reset(address?: string): void {
        this.manager.clean(address);
    }
}

// Usage
const utxoService = new UTXOService(provider);

const balance = await utxoService.getBalance(wallet.p2tr);
console.log('Balance:', balance);

const utxos = await utxoService.getSpendableUTXOs(wallet.p2tr, 100000n);
console.log('Selected UTXOs:', utxos.length);

const counts = await utxoService.countUTXOs(wallet.p2tr);
console.log('Confirmed:', counts.confirmed, 'Pending:', counts.pending);