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.
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.
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:
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
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
// 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.
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
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
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.
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
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
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.
// 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.
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.
// 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.
// 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
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);