UNPKG

@btc-stamps/tx-builder

Version:

Transaction builder for Bitcoin Stamps and SRC-20 tokens with advanced UTXO selection

1,504 lines (1,499 loc) 123 kB
// src/interfaces/selector-result.interface.ts function createSelectionSuccess(inputs, totalValue, change, fee, options) { const inputCount = inputs.length; const outputCount = options?.outputCount || 2; const estimatedVSize = options?.estimatedVSize || inputCount * 148 + outputCount * 34 + 10; return { success: true, inputs, totalValue, change, fee, wasteMetric: options?.wasteMetric, inputCount, outputCount, estimatedVSize, effectiveFeeRate: fee / estimatedVSize }; } function createSelectionFailure(reason, message, details) { return { success: false, reason, message, details }; } // src/selectors/base-selector.ts var BaseSelector = class { DUST_THRESHOLD = 546; INPUT_SIZE = 148; // Approximate size of a legacy input OUTPUT_SIZE = 34; // Approximate size of a P2PKH output TRANSACTION_OVERHEAD = 10; /** * Filter UTXOs based on confirmation requirements */ filterUTXOs(utxos, minConfirmations = 0) { return utxos.filter((utxo) => (utxo.confirmations ?? 0) >= minConfirmations); } /** * Filter UTXOs with protection and confirmation checks */ filterEligibleUTXOs(utxos, options) { let eligible = this.filterUTXOs(utxos, options.minConfirmations); if (options.protectedUTXODetector) { try { eligible = eligible.filter((utxo) => { try { return !options.protectedUTXODetector.isProtected(utxo); } catch { return true; } }); } catch { } } return eligible; } /** * Sort UTXOs by value (ascending) */ sortByValue(utxos, descending = false) { return [...utxos].sort((a, b) => descending ? b.value - a.value : a.value - b.value); } /** * Sort UTXOs by confirmations (most confirmed first) */ sortByConfirmations(utxos) { return [...utxos].sort((a, b) => (b.confirmations ?? 0) - (a.confirmations ?? 0)); } /** * Calculate total value of UTXOs */ sumUTXOs(utxos) { return utxos.reduce((sum, utxo) => sum + utxo.value, 0); } /** * Estimate transaction fee */ estimateFee(numInputs, numOutputs, feeRate) { const size = this.estimateTransactionSize(numInputs, numOutputs); return Math.ceil(size * feeRate); } /** * Estimate transaction size in vBytes */ estimateTransactionSize(numInputs, numOutputs) { return this.TRANSACTION_OVERHEAD + numInputs * this.INPUT_SIZE + numOutputs * this.OUTPUT_SIZE; } /** * Check if amount is dust */ isDust(amount, dustThreshold) { return amount < (dustThreshold ?? this.DUST_THRESHOLD); } /** * Calculate change amount */ calculateChange(inputValue, targetValue, fee) { return inputValue - targetValue - fee; } /** * Create selection result */ createResult(inputs, targetValue, feeRate, hasChange) { const totalValue = this.sumUTXOs(inputs); const numOutputs = hasChange ? 2 : 1; const fee = this.estimateFee(inputs.length, numOutputs, feeRate); const change = hasChange ? this.calculateChange(totalValue, targetValue, fee) : 0; const estimatedVSize = this.estimateTransactionSize(inputs.length, numOutputs); return { success: true, inputs, totalValue, change: hasChange ? change > 0 ? change : 0 : 0, fee, inputCount: inputs.length, outputCount: numOutputs, estimatedVSize, effectiveFeeRate: fee / estimatedVSize }; } /** * Validate selection options */ validateOptions(options) { if (options.targetValue <= 0) { throw new Error("Target value must be positive"); } if (options.feeRate <= 0) { throw new Error("Fee rate must be positive"); } if (options.maxInputs !== void 0 && options.maxInputs <= 0) { throw new Error("Max inputs must be positive"); } } /** * Check if options are valid and return failure result if not */ checkOptionsValidity(options) { if (options.targetValue <= 0) { return { success: false, reason: "INVALID_OPTIONS" /* INVALID_OPTIONS */, message: "Target value must be positive", details: { targetValue: options.targetValue } }; } if (options.feeRate <= 0) { return { success: false, reason: "INVALID_OPTIONS" /* INVALID_OPTIONS */, message: "Fee rate must be positive", details: { feeRate: options.feeRate } }; } if (options.maxInputs !== void 0 && options.maxInputs <= 0) { return { success: false, reason: "INVALID_OPTIONS" /* INVALID_OPTIONS */, message: "Max inputs must be positive", details: { maxInputs: options.maxInputs } }; } return null; } /** * Calculate waste metric for coin selection * Lower waste is better */ calculateWaste(inputs, targetValue, feeRate, longTermFeeRate = 10) { const totalValue = this.sumUTXOs(inputs); const currentFee = this.estimateFee(inputs.length, 2, feeRate); const change = totalValue - targetValue - currentFee; const changeCost = this.OUTPUT_SIZE * feeRate + this.INPUT_SIZE * longTermFeeRate; const excess = change > this.DUST_THRESHOLD ? 0 : change; const inputWaste = inputs.reduce((waste, _input) => { const currentCost = this.INPUT_SIZE * feeRate; const futureCost = this.INPUT_SIZE * longTermFeeRate; return waste + (currentCost - futureCost); }, 0); return changeCost + excess + inputWaste; } }; // src/selectors/accumulative.ts var AccumulativeSelector = class extends BaseSelector { getName() { return "accumulative"; } select(utxos, options) { const validationFailure = this.checkOptionsValidity(options); if (validationFailure) { return validationFailure; } const eligibleUTXOs = this.filterEligibleUTXOs(utxos, options); if (eligibleUTXOs.length === 0) { return { success: false, reason: "NO_UTXOS_AVAILABLE" /* NO_UTXOS_AVAILABLE */, message: "No eligible UTXOs available (confirmations/protection)", details: { utxoCount: utxos.length, minConfirmations: options.minConfirmations } }; } const sortedUTXOs = this.sortByValue(eligibleUTXOs, true); const selected = []; let accumulated = 0; let estimatedFee = this.estimateFee(1, 2, options.feeRate); let target = options.targetValue + estimatedFee; for (const utxo of sortedUTXOs) { if (options.maxInputs && selected.length >= options.maxInputs) { break; } selected.push(utxo); accumulated += utxo.value; estimatedFee = this.estimateFee(selected.length, 2, options.feeRate); target = options.targetValue + estimatedFee; if (accumulated >= target) { const change = accumulated - options.targetValue - estimatedFee; if (change < (options.dustThreshold ?? this.DUST_THRESHOLD)) { estimatedFee = this.estimateFee(selected.length, 1, options.feeRate); target = options.targetValue + estimatedFee; if (accumulated >= target) { return this.createResult( selected, options.targetValue, options.feeRate, false // No change output ); } } else { return this.createResult( selected, options.targetValue, options.feeRate, true // Has change output ); } } } if (accumulated >= target) { const change = accumulated - options.targetValue - estimatedFee; const hasChange = change >= (options.dustThreshold ?? this.DUST_THRESHOLD); return this.createResult( selected, options.targetValue, options.feeRate, hasChange ); } return { success: false, reason: "INSUFFICIENT_FUNDS" /* INSUFFICIENT_FUNDS */, message: "Insufficient funds to meet target value", details: { availableBalance: accumulated, requiredAmount: target, utxoCount: selected.length } }; } /** * Variant that prioritizes older UTXOs (FIFO) */ selectFIFO(utxos, options) { const validationFailure = this.checkOptionsValidity(options); if (validationFailure) { return validationFailure; } const eligibleUTXOs = this.filterEligibleUTXOs(utxos, options); if (eligibleUTXOs.length === 0) { return { success: false, reason: "NO_UTXOS_AVAILABLE" /* NO_UTXOS_AVAILABLE */, message: "No eligible UTXOs available (confirmations/protection)", details: { utxoCount: utxos.length, minConfirmations: options.minConfirmations } }; } const sortedUTXOs = this.sortByConfirmations(eligibleUTXOs); return this.selectFromSorted(sortedUTXOs, options); } /** * Variant that consolidates UTXOs */ selectForConsolidation(utxos, options) { const validationFailure = this.checkOptionsValidity(options); if (validationFailure) { return validationFailure; } const eligibleUTXOs = this.filterEligibleUTXOs(utxos, options); if (eligibleUTXOs.length === 0) { return { success: false, reason: "NO_UTXOS_AVAILABLE" /* NO_UTXOS_AVAILABLE */, message: "No eligible UTXOs available for consolidation", details: { utxoCount: utxos.length, minConfirmations: options.minConfirmations } }; } const sortedUTXOs = this.sortByValue(eligibleUTXOs, false); const maxInputs = options.maxInputs ?? Math.min(100, sortedUTXOs.length); const selected = sortedUTXOs.slice(0, maxInputs); const accumulated = this.sumUTXOs(selected); const estimatedFee = this.estimateFee(selected.length, 1, options.feeRate); const outputValue = accumulated - estimatedFee; if (outputValue < options.targetValue) { return { success: false, reason: "INSUFFICIENT_FUNDS" /* INSUFFICIENT_FUNDS */, message: "Insufficient funds after fees for consolidation", details: { availableBalance: outputValue, requiredAmount: options.targetValue, utxoCount: selected.length } }; } return this.createResult( selected, outputValue, // Use output value as target options.feeRate, false // No change in consolidation ); } /** * Helper method to select from pre-sorted UTXOs */ selectFromSorted(sortedUTXOs, options) { const selected = []; let accumulated = 0; let estimatedFee = this.estimateFee(1, 2, options.feeRate); let target = options.targetValue + estimatedFee; for (const utxo of sortedUTXOs) { if (options.maxInputs && selected.length >= options.maxInputs) { break; } selected.push(utxo); accumulated += utxo.value; estimatedFee = this.estimateFee(selected.length, 2, options.feeRate); target = options.targetValue + estimatedFee; if (accumulated >= target) { const change = accumulated - options.targetValue - estimatedFee; const hasChange = change >= (options.dustThreshold ?? this.DUST_THRESHOLD); if (!hasChange) { estimatedFee = this.estimateFee(selected.length, 1, options.feeRate); target = options.targetValue + estimatedFee; } if (accumulated >= target) { return this.createResult( selected, options.targetValue, options.feeRate, hasChange ); } } } return { success: false, reason: "INSUFFICIENT_FUNDS" /* INSUFFICIENT_FUNDS */, message: "Insufficient funds to meet target value", details: { availableBalance: accumulated, requiredAmount: target, utxoCount: selected.length } }; } }; // src/selectors/branch-and-bound.ts var BranchAndBoundSelector = class extends BaseSelector { MAX_ITERATIONS = 1e5; MAX_DEPTH = 20; // Prevent stack overflow COST_OF_CHANGE = 68; // ~68 vBytes for change output creation + future spending LONG_TERM_FEE_RATE = 10; // Default long-term fee rate for waste calculation getName() { return "branch-and-bound"; } // Ensure estimateFee is accessible (inherited from BaseSelector) estimateFee(numInputs, numOutputs, feeRate) { return super.estimateFee(numInputs, numOutputs, feeRate); } select(utxos, options) { console.log(`SELECT START: ${utxos.length} UTXOs, target=${options.targetValue}`); const validationFailure = this.checkOptionsValidity(options); if (validationFailure) { return validationFailure; } const filteredUTXOs = this.filterEligibleUTXOs(utxos, options); console.log(`FILTERED: ${filteredUTXOs.length} UTXOs after filtering`); if (filteredUTXOs.length === 0) { return createSelectionFailure( "NO_UTXOS_AVAILABLE" /* NO_UTXOS_AVAILABLE */, "No UTXOs available after filtering", { availableBalance: 0, requiredAmount: options.targetValue, utxoCount: 0 } ); } const sortedUTXOs = this.sortByValue(filteredUTXOs, true); console.log(`AFTER SORTING: ${sortedUTXOs.length} UTXOs`); const maxInputs = Math.min( options.maxInputs || 20, sortedUTXOs.length, this.MAX_DEPTH ); const searchSpace = sortedUTXOs.slice(0, maxInputs); const totalAvailable = this.sumUTXOs(filteredUTXOs); console.log(`TOTAL AVAILABLE: ${totalAvailable}`); const minFee = this.estimateFee(1, 1, options.feeRate); const minRequired = options.targetValue + minFee; console.log(`MIN REQUIRED: ${minRequired}`); if (totalAvailable < minRequired) { return createSelectionFailure( "INSUFFICIENT_FUNDS" /* INSUFFICIENT_FUNDS */, `Insufficient funds: have ${totalAvailable}, need at least ${minRequired}`, { availableBalance: totalAvailable, requiredAmount: minRequired, utxoCount: filteredUTXOs.length, targetValue: options.targetValue } ); } const changelessResult = this.findChangelessTransaction( searchSpace, options ); if (changelessResult.success) { return changelessResult; } const withChangeResult = this.findBestWithChange(searchSpace, options); if (withChangeResult.success) { return withChangeResult; } const simpleResult = this.simpleAccumulativeSelection(sortedUTXOs, options); return simpleResult; } /** * Find changeless transaction using optimized branch and bound * This is the core algorithm matching Bitcoin Core's implementation */ findChangelessTransaction(utxos, options) { if (utxos.length === 0) { return createSelectionFailure( "NO_UTXOS_AVAILABLE" /* NO_UTXOS_AVAILABLE */, "No UTXOs available for changeless selection", { availableBalance: 0, requiredAmount: options.targetValue, utxoCount: 0 } ); } let bestCandidate = null; let iterations = 0; const cumulativeValues = this.computeCumulativeValues(utxos); this.branchAndBoundRecursive( utxos, cumulativeValues, options, { selection: new Array(utxos.length).fill(false), totalValue: 0, utxoIndex: 0, depth: 0 }, (candidate) => { iterations++; if (iterations >= this.MAX_ITERATIONS) return true; if (this.isChangelessCandidate(candidate, options)) { const wasteScore = this.calculateChangelessWaste( candidate.utxos, options.targetValue, options.feeRate ); if (!bestCandidate || wasteScore < bestCandidate.wasteScore) { bestCandidate = { ...candidate, wasteScore, hasChange: false }; } } return false; } ); if (bestCandidate) { const candidate = bestCandidate; const totalValue2 = this.sumUTXOs(candidate.utxos); const fee = this.estimateFee(candidate.utxos.length, 1, options.feeRate); const change = totalValue2 - options.targetValue - fee; const estimatedVSize = this.estimateTransactionSize(candidate.utxos.length, 1); return createSelectionSuccess( candidate.utxos, totalValue2, change, fee, { wasteMetric: candidate.wasteScore, outputCount: 1, // changeless transaction estimatedVSize } ); } const totalValue = this.sumUTXOs(utxos); const estimatedFee = this.estimateFee(1, 1, options.feeRate); const requiredAmount = options.targetValue + estimatedFee; return createSelectionFailure( "NO_SOLUTION_FOUND" /* NO_SOLUTION_FOUND */, "No changeless solution found", { availableBalance: totalValue, requiredAmount, utxoCount: utxos.length, targetValue: options.targetValue } ); } /** * Compute cumulative values for efficient pruning */ computeCumulativeValues(utxos) { const cumulative = new Array(utxos.length); let sum = 0; for (let i = utxos.length - 1; i >= 0; i--) { sum += utxos[i].value; cumulative[i] = sum; } return cumulative; } /** * Recursive branch and bound implementation with efficient pruning */ branchAndBoundRecursive(utxos, cumulativeValues, options, state, onCandidate) { if (state.depth >= this.MAX_DEPTH) return false; if (state.utxoIndex >= utxos.length) { if (state.totalValue > 0) { const selectedUTXOs = utxos.filter((_, i) => state.selection[i]); const candidate = { utxos: selectedUTXOs, totalValue: state.totalValue, wasteScore: 0, // Will be calculated by callback hasChange: false // Will be determined by callback }; return onCandidate(candidate); } return false; } const currentUTXO = utxos[state.utxoIndex]; const requiredValue = this.calculateRequiredValue(options, 1); if (state.totalValue + (cumulativeValues[state.utxoIndex] ?? 0) < requiredValue) { return false; } const upperBound = requiredValue + this.COST_OF_CHANGE * options.feeRate; if (state.totalValue > upperBound) { return false; } let shouldStop = false; state.selection[state.utxoIndex] = true; state.totalValue += currentUTXO.value; state.utxoIndex++; state.depth++; shouldStop = this.branchAndBoundRecursive( utxos, cumulativeValues, options, state, onCandidate ); state.depth--; state.utxoIndex--; state.totalValue -= currentUTXO.value; state.selection[state.utxoIndex] = false; if (shouldStop) return true; state.utxoIndex++; state.depth++; shouldStop = this.branchAndBoundRecursive( utxos, cumulativeValues, options, state, onCandidate ); state.depth--; state.utxoIndex--; return shouldStop; } /** * Calculate required value for target plus fees */ calculateRequiredValue(options, numOutputs) { const minInputs = 1; const estimatedFee = this.estimateFee( minInputs, numOutputs, options.feeRate ); return options.targetValue + estimatedFee; } /** * Check if candidate is suitable for changeless transaction */ isChangelessCandidate(candidate, options) { const fee = this.estimateFee(candidate.utxos.length, 1, options.feeRate); const requiredValue = options.targetValue + fee; const excess = candidate.totalValue - requiredValue; const dustThreshold = options.dustThreshold || this.DUST_THRESHOLD; return excess >= 0 && excess <= dustThreshold; } /** * Calculate waste for changeless transactions */ calculateChangelessWaste(utxos, targetValue, feeRate) { const totalValue = this.sumUTXOs(utxos); const fee = this.estimateFee(utxos.length, 1, feeRate); const excess = totalValue - targetValue - fee; const inputWaste = utxos.reduce((waste, _) => { const currentCost = this.INPUT_SIZE * feeRate; const futureCost = this.INPUT_SIZE * this.LONG_TERM_FEE_RATE; return waste + Math.max(0, currentCost - futureCost); }, 0); return excess + inputWaste; } /** * Find best selection when change is needed * Uses a more efficient approach than exhaustive search */ findBestWithChange(utxos, options) { if (utxos.length === 0) { return createSelectionFailure( "NO_UTXOS_AVAILABLE" /* NO_UTXOS_AVAILABLE */, "No UTXOs available for selection with change", { availableBalance: 0, requiredAmount: options.targetValue, utxoCount: 0 } ); } let bestCandidate = null; let iterations = 0; const dustThreshold = options.dustThreshold || this.DUST_THRESHOLD; const minFee = this.estimateFee(1, 2, options.feeRate); const minRequired = options.targetValue + minFee + dustThreshold; const cumulativeValues = this.computeCumulativeValues(utxos); this.branchAndBoundRecursive( utxos, cumulativeValues, options, { selection: new Array(utxos.length).fill(false), totalValue: 0, utxoIndex: 0, depth: 0 }, (candidate) => { iterations++; if (iterations >= this.MAX_ITERATIONS) return true; if (this.isValidWithChange(candidate, options, minRequired)) { const wasteScore = this.calculateWasteWithChange( candidate.utxos, options.targetValue, options.feeRate ); if (!bestCandidate || wasteScore < bestCandidate.wasteScore) { bestCandidate = { ...candidate, wasteScore, hasChange: true }; } } return false; } ); if (bestCandidate) { const candidate = bestCandidate; const totalValue = this.sumUTXOs(candidate.utxos); const fee = this.estimateFee(candidate.utxos.length, 2, options.feeRate); const change = totalValue - options.targetValue - fee; const estimatedVSize = this.estimateTransactionSize(candidate.utxos.length, 2); return createSelectionSuccess( candidate.utxos, totalValue, change, fee, { wasteMetric: candidate.wasteScore, outputCount: 2, // target + change estimatedVSize } ); } return this.fallbackAccumulative(utxos, options); } /** * Check if candidate is valid for transaction with change */ isValidWithChange(candidate, options, minRequired) { if (candidate.totalValue < minRequired) return false; const fee = this.estimateFee(candidate.utxos.length, 2, options.feeRate); const change = candidate.totalValue - options.targetValue - fee; const dustThreshold = options.dustThreshold || this.DUST_THRESHOLD; return change >= dustThreshold; } /** * Calculate waste for transactions with change */ calculateWasteWithChange(utxos, targetValue, feeRate) { return this.calculateWaste( utxos, targetValue, feeRate, this.LONG_TERM_FEE_RATE ); } /** * Fallback to accumulative selection if B&B fails */ fallbackAccumulative(utxos, options) { const selected = []; let totalValue = 0; for (const utxo of utxos) { selected.push(utxo); totalValue += utxo.value; const fee = this.estimateFee(selected.length, 2, options.feeRate); const required = options.targetValue + fee; if (totalValue >= required) { const change = totalValue - options.targetValue - fee; const dustThreshold = options.dustThreshold || this.DUST_THRESHOLD; if (change >= dustThreshold) { const estimatedVSize = this.estimateTransactionSize(selected.length, 2); const wasteMetric = this.calculateWasteWithChange( selected, options.targetValue, options.feeRate ); return createSelectionSuccess( selected, totalValue, change, fee, { wasteMetric, outputCount: 2, // target + change estimatedVSize } ); } else if (change <= dustThreshold) { const changelessFee = this.estimateFee(selected.length, 1, options.feeRate); const changelessExcess = totalValue - options.targetValue - changelessFee; if (changelessExcess >= 0 && changelessExcess <= dustThreshold) { const estimatedVSize = this.estimateTransactionSize(selected.length, 1); const wasteMetric = this.calculateChangelessWaste( selected, options.targetValue, options.feeRate ); return createSelectionSuccess( selected, totalValue, changelessExcess, changelessFee, { wasteMetric, outputCount: 1, // changeless estimatedVSize } ); } } } if (selected.length >= (options.maxInputs || 20)) { break; } } const availableBalance = this.sumUTXOs(utxos); const estimatedFee = this.estimateFee(1, 2, options.feeRate); const requiredAmount = options.targetValue + estimatedFee; return createSelectionFailure( "SELECTION_FAILED" /* SELECTION_FAILED */, "Accumulative fallback failed to find suitable UTXOs", { availableBalance, requiredAmount, utxoCount: utxos.length, targetValue: options.targetValue } ); } /** * Simple accumulative selection as ultimate fallback * This method tries to find optimal solutions by considering changeless first */ simpleAccumulativeSelection(utxos, options) { console.log( `SimpleAccumulative: utxos=${utxos.length}, target=${options.targetValue}, totalAvailable=${this.sumUTXOs(utxos)}` ); if (utxos.length === 0) { return createSelectionFailure( "NO_UTXOS_AVAILABLE" /* NO_UTXOS_AVAILABLE */, "No UTXOs available for simple accumulative selection", { utxoCount: 0, requiredAmount: options.targetValue } ); } const dustThreshold = options.dustThreshold || this.DUST_THRESHOLD; const changelessResult = this.findOptimalChangeless(utxos, options, dustThreshold); if (changelessResult.success) { return changelessResult; } return this.fallbackAccumulativeWithChange(utxos, options, dustThreshold); } /** * Try to find optimal changeless solutions */ findOptimalChangeless(utxos, options, dustThreshold) { let bestChangeless = null; for (const utxo of utxos) { const fee = this.estimateFee(1, 1, options.feeRate); const required = options.targetValue + fee; if (utxo.value >= required) { const excess = utxo.value - required; if (excess <= dustThreshold) { const waste = this.calculateChangelessWaste([utxo], options.targetValue, options.feeRate); if (!bestChangeless || waste < bestChangeless.waste) { bestChangeless = { utxos: [utxo], excess, fee, waste }; } } } } for (let i = 0; i < utxos.length && i < 3; i++) { for (let j = i + 1; j < utxos.length && j < 6; j++) { const combination = [utxos[i], utxos[j]]; const totalValue = this.sumUTXOs(combination); const fee = this.estimateFee(2, 1, options.feeRate); const required = options.targetValue + fee; if (totalValue >= required) { const excess = totalValue - required; if (excess <= dustThreshold) { const waste = this.calculateChangelessWaste( combination, options.targetValue, options.feeRate ); if (!bestChangeless || waste < bestChangeless.waste) { bestChangeless = { utxos: combination, excess, fee, waste }; } } } for (let k = j + 1; k < utxos.length && k < 8; k++) { const combination3 = [utxos[i], utxos[j], utxos[k]]; const totalValue3 = this.sumUTXOs(combination3); const fee3 = this.estimateFee(3, 1, options.feeRate); const required3 = options.targetValue + fee3; if (totalValue3 >= required3) { const excess3 = totalValue3 - required3; if (excess3 <= dustThreshold) { const waste3 = this.calculateChangelessWaste( combination3, options.targetValue, options.feeRate ); if (!bestChangeless || waste3 < bestChangeless.waste) { bestChangeless = { utxos: combination3, excess: excess3, fee: fee3, waste: waste3 }; } } } } } } if (bestChangeless) { const totalValue = this.sumUTXOs(bestChangeless.utxos); console.log( `Found changeless solution: inputs=${bestChangeless.utxos.length}, excess=${bestChangeless.excess}, waste=${bestChangeless.waste}` ); return createSelectionSuccess( bestChangeless.utxos, totalValue, bestChangeless.excess, bestChangeless.fee, { wasteMetric: bestChangeless.waste, outputCount: 1, estimatedVSize: this.estimateTransactionSize(bestChangeless.utxos.length, 1) } ); } return createSelectionFailure( "NO_SOLUTION_FOUND" /* NO_SOLUTION_FOUND */, "No changeless solution found", { utxoCount: utxos.length, targetValue: options.targetValue } ); } /** * Fallback accumulative selection with change */ fallbackAccumulativeWithChange(utxos, options, dustThreshold) { const selected = []; let totalValue = 0; for (const utxo of utxos) { selected.push(utxo); totalValue += utxo.value; const fee = this.estimateFee(selected.length, 2, options.feeRate); const required = options.targetValue + fee; if (totalValue >= required) { const change = totalValue - options.targetValue - fee; console.log( ` Testing with change: inputs=${selected.length}, totalValue=${totalValue}, required=${required}, change=${change}, dustThreshold=${dustThreshold}` ); if (change >= dustThreshold) { console.log( `SimpleAccumulative SUCCESS: inputs=${selected.length}, totalValue=${totalValue}, change=${change}, fee=${fee}` ); const wasteMetric = this.calculateWasteWithChange( selected, options.targetValue, options.feeRate ); return createSelectionSuccess( [...selected], totalValue, change, fee, { wasteMetric, outputCount: 2, estimatedVSize: this.estimateTransactionSize(selected.length, 2) } ); } } if (selected.length >= (options.maxInputs || 20)) { break; } } const availableBalance = this.sumUTXOs(utxos); const minFee = this.estimateFee(1, 1, options.feeRate); return createSelectionFailure( availableBalance < options.targetValue + minFee ? "INSUFFICIENT_FUNDS" /* INSUFFICIENT_FUNDS */ : "SELECTION_FAILED" /* SELECTION_FAILED */, `Simple accumulative selection failed: available=${availableBalance}, target=${options.targetValue}`, { availableBalance, requiredAmount: options.targetValue + minFee, utxoCount: utxos.length, targetValue: options.targetValue } ); } /** * Enhanced waste calculation with Bitcoin Core alignment */ calculateWaste(inputs, targetValue, feeRate, longTermFeeRate = this.LONG_TERM_FEE_RATE) { const totalValue = this.sumUTXOs(inputs); const currentFee = this.estimateFee(inputs.length, 2, feeRate); const change = totalValue - targetValue - currentFee; const changeCost = change > this.DUST_THRESHOLD ? this.OUTPUT_SIZE * feeRate + this.INPUT_SIZE * longTermFeeRate : 0; const excessCost = change > 0 && change <= this.DUST_THRESHOLD ? change : 0; const inputWaste = inputs.reduce((waste, _) => { const currentInputCost = this.INPUT_SIZE * feeRate; const futureInputCost = this.INPUT_SIZE * longTermFeeRate; return waste + Math.max(0, currentInputCost - futureInputCost); }, 0); return changeCost + excessCost + inputWaste; } /** * Get algorithm performance metrics */ getPerformanceMetrics() { return { maxIterations: this.MAX_ITERATIONS, maxDepth: this.MAX_DEPTH, costOfChange: this.COST_OF_CHANGE, longTermFeeRate: this.LONG_TERM_FEE_RATE }; } }; // src/selectors/blackjack.ts var BlackjackSelector = class extends BaseSelector { MAX_COMBINATIONS = 1e4; EXACT_MATCH_TOLERANCE = 0; // Satoshis tolerance for "exact" match getName() { return "blackjack"; } select(utxos, options) { const validationFailure = this.checkOptionsValidity(options); if (validationFailure) return validationFailure; const filteredUTXOs = this.filterEligibleUTXOs(utxos, options); if (filteredUTXOs.length === 0) { return { success: false, reason: "NO_UTXOS_AVAILABLE" /* NO_UTXOS_AVAILABLE */, message: "No UTXOs available after filtering", details: { availableBalance: 0, requiredAmount: options.targetValue, utxoCount: 0 } }; } const totalAvailable = this.sumUTXOs(filteredUTXOs); const minFee = this.estimateFee(1, 1, options.feeRate); const minRequiredAmount = options.targetValue + minFee; if (totalAvailable < minRequiredAmount) { return { success: false, reason: "INSUFFICIENT_FUNDS" /* INSUFFICIENT_FUNDS */, message: `Insufficient funds: have ${totalAvailable}, need at least ${minRequiredAmount}`, details: { availableBalance: totalAvailable, requiredAmount: minRequiredAmount, utxoCount: filteredUTXOs.length, targetValue: options.targetValue } }; } const sortedUTXOs = this.sortByValue(filteredUTXOs, false); const exactMatch = this.findExactMatch(sortedUTXOs, options); if (exactMatch.success) { return exactMatch; } const closestMatch = this.findClosestMatch(sortedUTXOs, options); if (!closestMatch.success) { const estimatedFee = this.estimateFee(2, 2, options.feeRate); const estimatedRequired = options.targetValue + estimatedFee; return { success: false, reason: "SELECTION_FAILED" /* SELECTION_FAILED */, message: "Blackjack algorithm failed to find suitable UTXOs", details: { availableBalance: totalAvailable, requiredAmount: estimatedRequired, utxoCount: filteredUTXOs.length, targetValue: options.targetValue } }; } return closestMatch; } /** * Find exact match for changeless transaction */ findExactMatch(utxos, options) { const maxInputs = Math.min(options.maxInputs || 10, utxos.length); for (let size = 1; size <= maxInputs; size++) { const exactMatch = this.findExactCombination(utxos, options, size); if (exactMatch) { return exactMatch; } } const totalValue = this.sumUTXOs(utxos); const estimatedFee = this.estimateFee(1, 1, options.feeRate); const requiredAmount = options.targetValue + estimatedFee; return { success: false, reason: "NO_SOLUTION_FOUND" /* NO_SOLUTION_FOUND */, message: "No exact match found", details: { availableBalance: totalValue, requiredAmount, utxoCount: utxos.length, targetValue: options.targetValue } }; } /** * Find exact combination of specific size */ findExactCombination(utxos, options, size) { const combinations = this.generateCombinations(utxos, size); let bestCandidate = null; for (const combination of combinations) { const totalValue2 = this.sumUTXOs(combination); const fee = this.estimateFee(combination.length, 1, options.feeRate); const required = options.targetValue + fee; const exactness = Math.abs(totalValue2 - required); if (exactness <= this.EXACT_MATCH_TOLERANCE) { const candidate = { utxos: combination, totalValue: totalValue2, exactness }; if (!bestCandidate || exactness < bestCandidate.exactness) { bestCandidate = candidate; } } } if (bestCandidate) { return this.createResult( bestCandidate.utxos, options.targetValue, options.feeRate, false // No change for exact match ); } const totalValue = this.sumUTXOs(utxos); const estimatedFee = this.estimateFee(1, 1, options.feeRate); const requiredAmount = options.targetValue + estimatedFee; return { success: false, reason: "NO_SOLUTION_FOUND" /* NO_SOLUTION_FOUND */, message: "No exact combination found", details: { availableBalance: totalValue, requiredAmount, utxoCount: utxos.length, targetValue: options.targetValue } }; } /** * Find closest match when exact match is not possible */ findClosestMatch(utxos, options) { const maxInputs = Math.min(options.maxInputs || 15, utxos.length); let bestCandidate = null; for (let size = 1; size <= maxInputs; size++) { const candidate = this.findBestCombinationOfSize(utxos, options, size); if (candidate && this.isValidCandidate(candidate, options)) { if (!bestCandidate || this.isBetterCandidate(candidate, bestCandidate, options)) { bestCandidate = candidate; } } } if (!bestCandidate) { const totalValue2 = this.sumUTXOs(utxos); const estimatedFee2 = this.estimateFee(1, 2, options.feeRate); const requiredAmount2 = options.targetValue + estimatedFee2; return { success: false, reason: "NO_SOLUTION_FOUND" /* NO_SOLUTION_FOUND */, message: "No suitable closest match found", details: { availableBalance: totalValue2, requiredAmount: requiredAmount2, utxoCount: utxos.length, targetValue: options.targetValue } }; } const fee = this.estimateFee( bestCandidate.utxos.length, 2, options.feeRate ); const change = bestCandidate.totalValue - options.targetValue - fee; const dustThreshold = options.dustThreshold || this.DUST_THRESHOLD; const hasChange = change >= dustThreshold; if (!hasChange) { const singleOutputFee = this.estimateFee( bestCandidate.utxos.length, 1, options.feeRate ); const requiredForSingleOutput = options.targetValue + singleOutputFee; if (bestCandidate.totalValue >= requiredForSingleOutput) { return this.createResult( bestCandidate.utxos, options.targetValue, options.feeRate, false ); } } if (hasChange) { const result = this.createResult( bestCandidate.utxos, options.targetValue, options.feeRate, true ); result.wasteMetric = this.calculateWaste( bestCandidate.utxos, options.targetValue, options.feeRate ); return result; } const totalValue = this.sumUTXOs(utxos); const estimatedFee = this.estimateFee(1, 2, options.feeRate); const requiredAmount = options.targetValue + estimatedFee; return { success: false, reason: "DUST_OUTPUT" /* DUST_OUTPUT */, message: "Cannot create valid output - would create dust", details: { availableBalance: totalValue, requiredAmount, utxoCount: utxos.length, targetValue: options.targetValue, dustThreshold: options.dustThreshold || this.DUST_THRESHOLD } }; } /** * Find best combination of specific size */ findBestCombinationOfSize(utxos, options, size) { const combinations = this.generateCombinations(utxos, size); let bestCandidate = null; for (const combination of combinations) { const totalValue = this.sumUTXOs(combination); const changelessFee = this.estimateFee( combination.length, 1, options.feeRate ); const changelessRequired = options.targetValue + changelessFee; const changelessExactness = Math.abs(totalValue - changelessRequired); const withChangeFee = this.estimateFee( combination.length, 2, options.feeRate ); const withChangeRequired = options.targetValue + withChangeFee; const withChangeExactness = totalValue >= withChangeRequired ? Math.abs(totalValue - withChangeRequired) : Infinity; const exactness = changelessExactness <= withChangeExactness ? changelessExactness : withChangeExactness; const candidate = { utxos: combination, totalValue, exactness }; if (!bestCandidate || exactness < bestCandidate.exactness) { bestCandidate = candidate; } } return bestCandidate; } /** * Generate combinations of UTXOs */ generateCombinations(utxos, size) { const combinations = []; const maxCombinations = Math.min( this.MAX_COMBINATIONS, this.binomialCoefficient(utxos.length, size) ); this.generateCombinationsRecursive( utxos, size, 0, [], combinations, maxCombinations ); return combinations; } /** * Recursive combination generation with limit */ generateCombinationsRecursive(utxos, size, startIndex, current, results, maxResults) { if (results.length >= maxResults) return; if (current.length === size) { results.push([...current]); return; } const remaining = size - current.length; const available = utxos.length - startIndex; if (remaining > available) return; for (let i = startIndex; i <= utxos.length - remaining && results.length < maxResults; i++) { current.push(utxos[i]); this.generateCombinationsRecursive( utxos, size, i + 1, current, results, maxResults ); current.pop(); } } /** * Calculate binomial coefficient (n choose k) */ binomialCoefficient(n, k) { if (k > n) return 0; if (k === 0 || k === n) return 1; k = Math.min(k, n - k); let result = 1; for (let i = 0; i < k; i++) { result = result * (n - i) / (i + 1); } return Math.floor(result); } /** * Check if candidate is valid for transaction */ isValidCandidate(candidate, options) { const minFee = this.estimateFee(candidate.utxos.length, 1, options.feeRate); const minRequired = options.targetValue + minFee; return candidate.totalValue >= minRequired; } /** * Compare two candidates to determine which is better */ isBetterCandidate(candidate1, candidate2, options) { const isVeryExact1 = candidate1.exactness <= this.EXACT_MATCH_TOLERANCE; const isVeryExact2 = candidate2.exactness <= this.EXACT_MATCH_TOLERANCE; if (isVeryExact1 !== isVeryExact2) { return isVeryExact1; } const minFee1 = this.estimateFee(candidate1.utxos.length, 1, options.feeRate); const minRequired1 = options.targetValue + minFee1; const meetsMin1 = candidate1.totalValue >= minRequired1; const minFee2 = this.estimateFee(candidate2.utxos.length, 1, options.feeRate); const minRequired2 = options.targetValue + minFee2; const meetsMin2 = candidate2.totalValue >= minRequired2; if (meetsMin1 !== meetsMin2) { return meetsMin1; } if (meetsMin1 && meetsMin2) { if (candidate1.totalValue !== candidate2.totalValue) { return candidate1.totalValue < candidate2.totalValue; } } if (candidate1.exactness !== candidate2.exactness) { return candidate1.exactness < candidate2.exactness; } if (candidate1.utxos.length !== candidate2.utxos.length) { return candidate1.utxos.length < candidate2.utxos.length; } return candidate1.totalValue < candidate2.totalValue; } /** * Optimized selection for specific target amounts * Uses dynamic programming approach for better performance */ selectOptimized(utxos, options) { const validationFailure = this.checkOptionsValidity(options); if (validationFailure) return validationFailure; const filteredUTXOs = this.filterEligibleUTXOs(utxos, options); if (filteredUTXOs.length === 0) { return { success: false, reason: "NO_UTXOS_AVAILABLE" /* NO_UTXOS_AVAILABLE */, message: "No UTXOs available after filtering", details: { availableBalance: 0, requiredAmount: options.targetValue, utxoCount: 0 } }; } const targetWithFee = options.targetValue + this.estimateFee(2, 1, options.feeRate); const result = this.subsetSum( filteredUTXOs, targetWithFee, options.maxInputs || 10 ); if (result.length > 0) { return this.createResult( result, options.targetValue, options.feeRate, false ); } return this.select(utxos, options); } /** * Subset sum algorithm for exact matching */ subsetSum(utxos, target, maxInputs) { const n = Math.min(utxos.length, maxInputs); const dp = Array(n + 1).fill(null).map(() => Array(target + 1).fill(false)); for (let i = 0; i <= n; i++) { dp[i][0] = true; } for (let i = 1; i <= n; i++) { const utxo = utxos[i - 1]; for (let sum = 1; sum <= target; sum++) { dp[i][sum] = dp[i - 1][sum] || false; if (sum >= utxo.value && dp[i - 1][sum - utxo.value]) { dp[i][sum] = true; } } } if (!dp[n][target]) { return []; } const result = []; let currentSum = target; for (let i = n; i > 0 && currentSum > 0; i--) { if (!dp[i - 1][currentSum]) { result.push(utxos[i - 1]); currentSum -= utxos[i - 1].value; } } return result.reverse(); } /** * Get algorithm statistics */ getStats() { return { maxCombinations: this.MAX_COMBINATIONS, exactMatchTolerance: this.EXACT_MATCH_TOLERANCE }; } }; // src/selectors/waste-optimized.ts var WasteOptimizedSelector = class extends BaseSelector { algorithms; config; constructor(config) { super(); this.config = { algorithms: ["branch-and-bound", "accumulative", "blackjack"], maxExecutionTime: 5e3, // 5 seconds parallelExecution: false, wasteWeighting: { changeCost: 1, excessCost: 0.5, inputCost: 0.1 }, ...config }; this.algorithms = /* @__PURE__ */ new Map(); this.algorithms.set("accumulative", new AccumulativeSelector()); this.algorithms.set("branch-and-bound", new BranchAndBoundSelector()); this.algorithms.set("blackjack", new BlackjackSelector()); } getName() { return "waste-optimized"; } select(utxos, options) { try { const validationFailure = this.checkOptionsValidity(options); if (validationFailure) { return validationFailure; } const startTime = Date.now(); const { filteredUTXOs: usableUtxos, dustUTXOs, lowConfirmationUTXOs, protectedUTXOs } = this.filterUsableUtxos(utxos, options); if (usableUtxos.length === 0) { if (utxos.length === 0) { return this.createFailureResult( "NO_UTXOS_AVAILABLE" /* NO_UTXOS_AVAILABLE */, "No UTXOs available for selection", { utxoCount: 0 } ); } const totalFiltered = dustUTXOs.length + lowConfirmationUTXOs.length + protectedUTXOs.length; if (protectedUTXOs.length === utxos.length) { return this.createFailureResult( "NO_UTXOS_AVAILABLE" /* NO_UTXOS_AVAILABLE */, "No UTXOs available - all are protected", { utxoCount: utxos.length, protectedCount: protectedUTXOs.length, originalReason: "PROTECTED_UTXOS" } ); } else if (dustUTXOs.length > 0 && dustUTXOs.length + protectedUTXOs.length === utxos.length) { return this.createFailureResult( "INSUFFICIENT_FUNDS" /* INSUFFICIENT_FUNDS */, `Insufficient funds - all unprotected UTXOs are below dust threshold of ${options.dustThreshold || 546} satoshis`, { utxoCount: utxos.length, dustCount: dustUTXOs.length, protectedCount: protectedUTXOs.length, dustThreshold: options.dustThreshold || 546 } ); } else if (lowConfirmationUTXOs.length > 0 && totalFiltered === utxos.length) { return this.createFailureResult( "NO_UTXOS_AVAILABLE" /* NO_UTXOS_AVAILABLE */, `No UTXOs available - insufficient confirmations`, { utxoCount: utxos.length, lowConfirmationCount: lowConfirmationUTXOs.length, dustCount: dustUTXOs.length, protecte