/**
* UTXO Manager - Phase 6.3
*
* Manages Unspent Transaction Outputs (UTXOs) for Bitcoin wallet.
* Tracks UTXOs in Autobase for multi-device synchronization.
*/
const b4a = require('b4a')
/**
* Create UTXO key for Autobase storage
* Format: utxo:{chain}:{txid}:{vout}
*/
function createUTXOKey(chain, txid, vout) {
return `utxo:${chain}:${txid}:${vout}`
}
/**
* Store UTXO in Autobase
* @param {Object} utxo - UTXO data from blockchain
* @param {string} chain - Chain identifier (bitcoin, ethereum)
* @param {Function} storeUTXOFn - Backend storeUTXO function
*/
async function storeUTXO(utxo, chain, storeUTXOFn) {
const utxoData = {
chain,
txid: utxo.txid,
vout: utxo.vout,
address: utxo.address,
amount: utxo.value, // satoshis
scriptPubKey: utxo.scriptPubKey || null,
confirmations: utxo.confirmations || 0,
blockHeight: utxo.blockHeight || null,
spendable: true,
spent: false,
spentTxid: null,
timestamp: Date.now()
}
await storeUTXOFn(utxoData)
return utxoData
}
/**
* Mark UTXO as spent
* @param {string} txid - Transaction ID
* @param {number} vout - Output index
* @param {string} spentTxid - Transaction that spent this UTXO
* @param {string} chain - Chain identifier
* @param {Function} updateUTXOFn - Backend updateUTXO function
*/
async function markUTXOSpent(txid, vout, spentTxid, chain, updateUTXOFn) {
await updateUTXOFn(chain, txid, vout, {
spent: true,
spendable: false,
spentTxid,
spentAt: Date.now()
})
}
/**
* Calculate balance from UTXOs
* @param {Array} utxos - Array of UTXO objects
* @returns {Object} Balance breakdown
*/
function calculateBalance(utxos) {
const confirmed = utxos
.filter(u => u.confirmations >= 6 && !u.spent && u.spendable)
.reduce((sum, u) => sum + u.amount, 0)
const unconfirmed = utxos
.filter(u => u.confirmations > 0 && u.confirmations < 6 && !u.spent && u.spendable)
.reduce((sum, u) => sum + u.amount, 0)
const pending = utxos
.filter(u => u.confirmations === 0 && !u.spent && u.spendable)
.reduce((sum, u) => sum + u.amount, 0)
return {
confirmed, // 6+ confirmations
unconfirmed, // 1-5 confirmations
pending, // 0 confirmations
total: confirmed + unconfirmed + pending,
spendable: confirmed + unconfirmed // Don't include pending in spendable
}
}
/**
* Get spendable UTXOs (confirmed + unconfirmed, not spent)
* @param {Array} utxos - Array of UTXO objects
* @param {boolean} confirmedOnly - Only return confirmed UTXOs (6+ confirmations)
* @returns {Array} Spendable UTXOs sorted by amount (largest first)
*/
function getSpendableUTXOs(utxos, confirmedOnly = false) {
const minConfirmations = confirmedOnly ? 6 : 1
return utxos
.filter(u =>
u.confirmations >= minConfirmations &&
!u.spent &&
u.spendable
)
.sort((a, b) => b.amount - a.amount) // Largest first for coin selection
}
/**
* Format satoshis to BTC display
* @param {number} satoshis - Amount in satoshis
* @returns {string} Formatted BTC amount
*/
function formatBTC(satoshis) {
const btc = (satoshis / 100000000).toFixed(8)
return btc.replace(/\.?0+$/, '') || '0' // Remove trailing zeros
}
/**
* Format balance display
* @param {Object} balance - Balance object from calculateBalance
* @param {string} symbol - Currency symbol (BTC, ETH, etc.)
* @returns {string} Formatted balance string
*/
function formatBalance(balance, symbol = 'BTC') {
const lines = []
if (balance.confirmed > 0) {
lines.push(`Confirmed: ${formatBTC(balance.confirmed)} ${symbol} (${balance.confirmed.toLocaleString()} sats)`)
}
if (balance.unconfirmed > 0) {
lines.push(`Unconfirmed: ${formatBTC(balance.unconfirmed)} ${symbol} (${balance.unconfirmed.toLocaleString()} sats)`)
}
if (balance.pending > 0) {
lines.push(`Pending: ${formatBTC(balance.pending)} ${symbol} (${balance.pending.toLocaleString()} sats)`)
}
if (lines.length === 0) {
return `Balance: 0 ${symbol}`
}
if (balance.total !== balance.confirmed) {
lines.push(`─────────────────────────────────────`)
lines.push(`Total: ${formatBTC(balance.total)} ${symbol} (${balance.total.toLocaleString()} sats)`)
}
return lines.join('\n')
}
module.exports = {
createUTXOKey,
storeUTXO,
markUTXOSpent,
calculateBalance,
getSpendableUTXOs,
formatBTC,
formatBalance
}