Bitcoin's UTXO (Unspent Transaction Output) model is a fundamental concept that differs significantly from the more familiar account-based model used in Ethereum and traditional banking systems. In this post, we'll dive into a practical TypeScript implementation of UTXO management. We'll explore how to fetch UTXOs from the blockchain, calculate balances, and select optimal inputs for new transactions - all essential operations for working with Bitcoin's transaction model. The complete source code for this implementation is available in this repository.
Unlike traditional banking systems where your balance is stored as a single number, Bitcoin doesn't actually store account balances at all. Instead, it maintains a list of Unspent Transaction Outputs (UTXOs) - essentially, a collection of "coins" that you own and can spend. Your actual balance is calculated by summing up the values of all your UTXOs.
import { sum } from "@lib/utils/array/sum"
import { ConfirmedUtxo } from "./utxo"
export const getBalance = (utxos: ConfirmedUtxo[]) => {
return sum(utxos.map((utxo) => utxo.value))
}
Think of UTXOs like physical cash and change in your wallet. When you pay for a $15 item with a $20 bill, you get $5 back in change. Similarly, when you spend Bitcoin, you must use complete UTXOs (like whole bills), and you get "change" back in the form of a new UTXO. For example, if you have a single UTXO worth 1 BTC and want to send someone 0.3 BTC, the transaction will create two new UTXOs: one worth 0.3 BTC for the recipient and another worth 0.7 BTC (minus fees) returned to you as change.
Each UTXO contains several important fields: txid
(the ID of the transaction that created this UTXO), vout
(the index of this output in the original transaction), value
(the amount in satoshis), and status
(whether the transaction is confirmed and its block details). A UTXO can be either confirmed (included in a block) or unconfirmed (waiting in the mempool).
type ConfirmedStatus = {
confirmed: true
block_height: number
block_hash: string
block_time: number
}
type UnconfirmedStatus = {
confirmed: false
}
export type Utxo = {
txid: string
vout: number
status: ConfirmedStatus | UnconfirmedStatus
value: number
}
export type ConfirmedUtxo = Omit<Utxo, "status"> & {
status: ConfirmedStatus
value: number
}
export const getConfirmedUtxos = (utxos: Utxo[]): ConfirmedUtxo[] => {
return utxos.filter((utxo): utxo is ConfirmedUtxo => utxo.status.confirmed)
}
Let's put our UTXO management code into practice by examining a real Bitcoin address. The following example demonstrates how to fetch UTXOs from the Blockstream API, filter them to include only confirmed transactions, and calculate the total balance. We'll use the queryUrl
utility to make the API request, our getConfirmedUtxos
function to filter the results, and the getBalance
function to sum up the UTXO values.
import { btc } from "@lib/chain/bitcoin/btc"
import { queryUrl } from "@lib/utils/query/queryUrl"
import { getConfirmedUtxos, Utxo } from "./core/utxo"
import { getBalance } from "./core/getBalance"
import { selectUtxos } from "./core/selectUtxos"
import { toChainAmount } from "@lib/chain/utils/toChainAmount"
import { sum } from "@lib/utils/array/sum"
import { formatChainAmount } from "@lib/chain/utils/formatChainAmount"
const test = async () => {
const address =
"bc1qa2eu6p5rl9255e3xz7fcgm6snn4wl5kdfh7zpt05qp5fad9dmsys0qjg0e"
console.log(`\nFetching UTXOs for address: ${address}`)
const utxos = await queryUrl<Utxo[]>(
`https://blockstream.info/api/address/${address}/utxo`,
)
const confirmedUtxos = getConfirmedUtxos(utxos)
console.log(`\nFound ${confirmedUtxos.length} UTXOs:`)
confirmedUtxos.forEach((utxo, index) => {
console.log(`\nUTXO #${index + 1}:`)
console.log(`Transaction ID: ${utxo.txid}`)
console.log(`Output Index: ${utxo.vout}`)
console.log(`Amount: ${formatChainAmount(utxo.value, btc)}`)
console.log(`Block Height: ${utxo.status.block_height}`)
console.log(`Block Time: ${utxo.status.block_time}`)
console.log(`Block Hash: ${utxo.status.block_hash}`)
})
const balance = getBalance(confirmedUtxos)
console.log(`\nTotal Balance: ${formatChainAmount(balance, btc)}`)
const btcToSpend = toChainAmount(10, btc.decimals)
const estimatedFee = BigInt(1000)
const btcTargetAmount = btcToSpend + estimatedFee
console.log(
`\nAttempting to select UTXOs for spending ${formatChainAmount(
btcTargetAmount,
btc,
)}...`,
)
const selectedUtxos = selectUtxos(confirmedUtxos, btcTargetAmount)
console.log("\nSelected UTXOs:")
const totalSelected = sum(selectedUtxos.map((utxo) => utxo.value))
selectedUtxos.forEach((utxo, index) => {
console.log(`\nInput #${index + 1}:`)
console.log(`Transaction ID: ${utxo.txid}`)
console.log(`Output Index: ${utxo.vout}`)
console.log(`Amount: ${formatChainAmount(utxo.value, btc)}`)
})
console.log("\nTransaction Summary:")
console.log(`Total Input Amount: ${formatChainAmount(totalSelected, btc)}`)
console.log(`Target Amount: ${formatChainAmount(btcTargetAmount, btc)}`)
console.log(`Network Fee: ${formatChainAmount(estimatedFee, btc)}`)
console.log(
`Change Amount: ${formatChainAmount(
BigInt(totalSelected) - btcTargetAmount,
btc,
)}`,
)
}
test()
When creating a Bitcoin transaction, we need to carefully select which UTXOs to use as inputs. Each UTXO included in a transaction increases its size in bytes, which directly affects the transaction fee - miners charge fees based on the transaction's size, not its value. For example, if the current fee rate is 50 sats/vByte and each input adds roughly 68 vBytes to the transaction, each additional UTXO would increase the fee by about 3,400 satoshis. This is why it's generally preferable to use fewer UTXOs when possible. Our UTXO selection algorithm below first tries to find a single UTXO that can cover the target amount (including fees). If that's not possible, it falls back to selecting multiple UTXOs, starting with the largest ones to minimize the number of inputs needed. For demonstration purposes, we'll use a simplified fixed fee estimate, but in a real implementation, you'd want to calculate the fee dynamically based on the current network fee rate and the number of inputs/outputs.
import { order } from "@lib/utils/array/order"
import { ConfirmedUtxo } from "./utxo"
export const selectUtxos = (
utxos: ConfirmedUtxo[],
targetAmount: bigint,
): ConfirmedUtxo[] => {
const sortedUtxos = order(utxos, ({ value }) => value, "asc")
const singleUtxo = sortedUtxos.find(
(utxo) => BigInt(utxo.value) >= targetAmount,
)
if (singleUtxo) {
return [singleUtxo]
}
const selectedUtxos: ConfirmedUtxo[] = []
let currentSum = 0
for (const utxo of sortedUtxos.toReversed()) {
selectedUtxos.push(utxo)
currentSum += utxo.value
if (currentSum >= targetAmount) {
return selectedUtxos
}
}
throw new Error("Insufficient funds to cover target amount")
}
Bitcoin amounts are typically stored and calculated in satoshis (the smallest unit of Bitcoin, where 1 BTC = 100,000,000 satoshis) to avoid floating-point precision issues. When working with the Bitcoin network, we convert between these two representations.
import { AssetInfo } from "../types/AssetInfo"
export const btc: AssetInfo = {
decimals: 8,
symbol: "BTC",
name: "Bitcoin",
}
This conversion ensures precise calculations when working with UTXOs while providing user-friendly BTC values in the interface.
import { AssetInfo } from "@lib/chain/types/AssetInfo"
import { fromChainAmount } from "./fromChainAmount"
import { formatAmount } from "@lib/utils/formatAmount"
export const formatChainAmount = (
amount: number | bigint,
{ decimals, symbol }: Pick<AssetInfo, "decimals" | "symbol">,
): string => {
return `${formatAmount(fromChainAmount(amount, decimals))} ${symbol}`
}
Understanding and implementing UTXO management is crucial for building Bitcoin applications. The TypeScript implementation we've explored demonstrates the core concepts: fetching UTXOs, calculating balances, and selecting optimal inputs for transactions. These fundamentals serve as a foundation for building more sophisticated Bitcoin applications.