UNPKG

j-bitcoin

Version:

Comprehensive JavaScript/TypeScript Bitcoin (BTC) wallet library with custodial and non-custodial support, hierarchical deterministic keys, threshold signatures, and advanced cryptographic features

254 lines (212 loc) 6.57 kB
/** * @fileoverview UTXO management with selection strategies * @version 1.0.0 * @author yfbsei * @license ISC */ class UTXOManagerError extends Error { constructor(message, code, details = {}) { super(message); this.name = 'UTXOManagerError'; this.code = code; this.details = details; } } const UTXO_CONSTANTS = { MAX_UTXOS_PER_TRANSACTION: 100, MIN_UTXO_VALUE: 546, MAX_SELECTION_ATTEMPTS: 1000, CONSOLIDATION_THRESHOLD: 100, DEFAULT_FEE_RATE: 10, MIN_FEE_RATE: 1, MAX_FEE_RATE: 1000 }; const INPUT_SIZES = { p2pkh: 148, p2sh: 91, p2wpkh: 68, p2wsh: 91, p2tr: 57.5 }; const OUTPUT_SIZES = { p2pkh: 34, p2sh: 32, p2wpkh: 31, p2wsh: 43, p2tr: 43 }; class UTXOManager { constructor(utxos = []) { this.utxos = []; this.feeCache = new Map(); for (const utxo of utxos) { this.addUTXO(utxo); } } addUTXO(utxo) { if (!utxo.txid || typeof utxo.txid !== 'string') { throw new UTXOManagerError('Invalid UTXO txid', 'INVALID_TXID'); } if (typeof utxo.vout !== 'number' || utxo.vout < 0) { throw new UTXOManagerError('Invalid UTXO vout', 'INVALID_VOUT'); } if (typeof utxo.value !== 'number' || utxo.value < 0) { throw new UTXOManagerError('Invalid UTXO value', 'INVALID_VALUE'); } this.utxos.push({ txid: utxo.txid, vout: utxo.vout, value: utxo.value, scriptPubKey: utxo.scriptPubKey, address: utxo.address, type: utxo.type || 'p2wpkh', confirmations: utxo.confirmations || 0, isSpent: false }); return this; } removeUTXO(txid, vout) { const index = this.utxos.findIndex(u => u.txid === txid && u.vout === vout); if (index !== -1) { this.utxos.splice(index, 1); } return this; } markSpent(txid, vout) { const utxo = this.utxos.find(u => u.txid === txid && u.vout === vout); if (utxo) { utxo.isSpent = true; } return this; } getBalance() { return this.utxos .filter(u => !u.isSpent) .reduce((sum, u) => sum + u.value, 0); } getAvailableUTXOs() { return this.utxos.filter(u => !u.isSpent); } selectUTXOs(targetAmount, feeRate = UTXO_CONSTANTS.DEFAULT_FEE_RATE, strategy = 'optimal') { const available = this.getAvailableUTXOs(); if (available.length === 0) { throw new UTXOManagerError('No available UTXOs', 'NO_UTXOS'); } switch (strategy) { case 'largest': return this._selectLargestFirst(available, targetAmount, feeRate); case 'smallest': return this._selectSmallestFirst(available, targetAmount, feeRate); case 'oldest': return this._selectOldestFirst(available, targetAmount, feeRate); case 'optimal': default: return this._selectOptimal(available, targetAmount, feeRate); } } _selectLargestFirst(utxos, targetAmount, feeRate) { const sorted = [...utxos].sort((a, b) => b.value - a.value); return this._accumulate(sorted, targetAmount, feeRate); } _selectSmallestFirst(utxos, targetAmount, feeRate) { const sorted = [...utxos].sort((a, b) => a.value - b.value); return this._accumulate(sorted, targetAmount, feeRate); } _selectOldestFirst(utxos, targetAmount, feeRate) { const sorted = [...utxos].sort((a, b) => b.confirmations - a.confirmations); return this._accumulate(sorted, targetAmount, feeRate); } _selectOptimal(utxos, targetAmount, feeRate) { const exactMatch = utxos.find(u => { const fee = this._estimateFee([u], 2, feeRate); return u.value === targetAmount + fee; }); if (exactMatch) { return { utxos: [exactMatch], totalValue: exactMatch.value, fee: this._estimateFee([exactMatch], 2, feeRate), change: 0 }; } return this._selectLargestFirst(utxos, targetAmount, feeRate); } _accumulate(sortedUtxos, targetAmount, feeRate) { const selected = []; let totalValue = 0; for (const utxo of sortedUtxos) { selected.push(utxo); totalValue += utxo.value; const fee = this._estimateFee(selected, 2, feeRate); const requiredAmount = targetAmount + fee; if (totalValue >= requiredAmount) { const change = totalValue - requiredAmount; return { utxos: selected, totalValue, fee, change: change >= UTXO_CONSTANTS.MIN_UTXO_VALUE ? change : 0 }; } if (selected.length >= UTXO_CONSTANTS.MAX_UTXOS_PER_TRANSACTION) { break; } } throw new UTXOManagerError( `Insufficient funds: have ${totalValue}, need ${targetAmount}`, 'INSUFFICIENT_FUNDS', { available: totalValue, required: targetAmount } ); } _estimateFee(inputs, outputCount, feeRate) { const baseSize = 10; let inputSize = 0; for (const input of inputs) { inputSize += INPUT_SIZES[input.type] || INPUT_SIZES.p2wpkh; } const outputSize = outputCount * OUTPUT_SIZES.p2wpkh; const virtualSize = baseSize + inputSize + outputSize; return Math.ceil(virtualSize * feeRate); } estimateFee(inputCount, outputCount, feeRate = UTXO_CONSTANTS.DEFAULT_FEE_RATE, inputType = 'p2wpkh') { const baseSize = 10; const inputSize = inputCount * (INPUT_SIZES[inputType] || INPUT_SIZES.p2wpkh); const outputSize = outputCount * OUTPUT_SIZES.p2wpkh; const virtualSize = baseSize + inputSize + outputSize; return Math.ceil(virtualSize * feeRate); } shouldConsolidate() { const available = this.getAvailableUTXOs(); return available.length >= UTXO_CONSTANTS.CONSOLIDATION_THRESHOLD; } getConsolidationUTXOs(maxCount = 50) { const available = this.getAvailableUTXOs(); const sorted = [...available].sort((a, b) => a.value - b.value); return sorted.slice(0, maxCount); } toJSON() { return { utxoCount: this.utxos.length, balance: this.getBalance(), utxos: this.utxos.map(u => ({ txid: u.txid, vout: u.vout, value: u.value, type: u.type, isSpent: u.isSpent })) }; } clear() { this.utxos = []; this.feeCache.clear(); return this; } } export { UTXOManager, UTXOManagerError, UTXO_CONSTANTS, INPUT_SIZES, OUTPUT_SIZES };