@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
JavaScript
// 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