bitcoin-utxo-select
Version:
Bitcoin utxo selections
129 lines (128 loc) • 5.36 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.finalize = exports.sumValues = exports.BLANK_OUTPUT = exports.transactionBytes = exports.dustThreshold = exports.sortUtxoBasedOnScore = exports.utxoScore = exports.outputBytes = exports.inputBytes = exports.TX_OUTPUT_SEGWIT_SCRIPTHASH = exports.TX_OUTPUT_SEGWIT = exports.TX_OUTPUT_SCRIPTHASH = exports.TX_OUTPUT_PUBKEYHASH = exports.TX_OUTPUT_BASE = exports.TX_INPUT_TAPROOT = exports.TX_INPUT_SEGWIT = exports.TX_INPUT_PUBKEYHASH = exports.TX_INPUT_BASE = exports.TX_EMPTY_SIZE = void 0;
// Refer to this page: https://bitcoinops.org/en/tools/calc-size/
exports.TX_EMPTY_SIZE = 4 + 1 + 1 + 4;
exports.TX_INPUT_BASE = 32 + 4 + 1 + 4;
exports.TX_INPUT_PUBKEYHASH = 107;
exports.TX_INPUT_SEGWIT = 27 + 1;
exports.TX_INPUT_TAPROOT = 17 + 1;
exports.TX_OUTPUT_BASE = 8 + 1;
exports.TX_OUTPUT_PUBKEYHASH = 25;
exports.TX_OUTPUT_SCRIPTHASH = 23;
exports.TX_OUTPUT_SEGWIT = 22;
exports.TX_OUTPUT_SEGWIT_SCRIPTHASH = 34;
// TODO: make it more secure and add some validations
function inputBytes(input) {
let bytes = exports.TX_INPUT_BASE;
if (input.redeemScript) {
bytes += input.redeemScript.length;
}
if (input.witnessScript) {
bytes += Math.ceil(input.witnessScript.byteLength / 4);
}
else if (input.isTaproot) {
if (input.taprootWitness) {
bytes += Math.ceil(exports.TX_INPUT_TAPROOT +
input.taprootWitness.reduce((prev, buffer) => prev + buffer.byteLength, 0) / 4);
}
else {
bytes += exports.TX_INPUT_TAPROOT;
}
}
else if (input.witnessUtxo) {
bytes += exports.TX_INPUT_SEGWIT;
}
else if (!input.redeemScript) {
bytes += exports.TX_INPUT_PUBKEYHASH;
}
return bytes;
}
exports.inputBytes = inputBytes;
function outputBytes(output) {
var _a, _b, _c, _d, _e, _f, _g;
let bytes = exports.TX_OUTPUT_BASE;
if (output.script) {
bytes += output.script.byteLength;
}
else if (((_a = output.address) === null || _a === void 0 ? void 0 : _a.startsWith('bc1')) || // mainnet
((_b = output.address) === null || _b === void 0 ? void 0 : _b.startsWith('tb1')) || // testnet
((_c = output.address) === null || _c === void 0 ? void 0 : _c.startsWith('bcrt1')) // regtest
) {
// 42 for mainnet/testnet, 44 for regtest
if (((_d = output.address) === null || _d === void 0 ? void 0 : _d.length) === 42 || ((_e = output.address) === null || _e === void 0 ? void 0 : _e.length) === 44) {
bytes += exports.TX_OUTPUT_SEGWIT;
}
else {
// taproot fee approximate is same like p2wsh (2 of 3 multisig)
bytes += exports.TX_OUTPUT_SEGWIT_SCRIPTHASH;
}
// both testnet and regtest has the same prefix 2
}
else if (((_f = output.address) === null || _f === void 0 ? void 0 : _f.startsWith('3')) || ((_g = output.address) === null || _g === void 0 ? void 0 : _g.startsWith('2'))) {
bytes += exports.TX_OUTPUT_SCRIPTHASH;
}
else {
bytes += exports.TX_OUTPUT_PUBKEYHASH;
}
return bytes;
}
exports.outputBytes = outputBytes;
// utxo minus the input approximate fee
function utxoScore(utxo, feeRate) {
return utxo.value - feeRate * inputBytes(utxo);
}
exports.utxoScore = utxoScore;
// order by descending
function sortUtxoBasedOnScore(utxos, feeRate) {
return utxos.sort((a, b) => {
return utxoScore(b, feeRate) - utxoScore(a, feeRate);
});
}
exports.sortUtxoBasedOnScore = sortUtxoBasedOnScore;
function dustThreshold(feeRate) {
// set the value from the highest input type
return exports.TX_INPUT_BASE + exports.TX_INPUT_PUBKEYHASH * feeRate;
}
exports.dustThreshold = dustThreshold;
function transactionBytes(inputs, outputs) {
return (exports.TX_EMPTY_SIZE +
inputs.reduce((prev, utxo) => prev + inputBytes(utxo), 0) +
outputs.reduce((prev, target) => prev + outputBytes(target), 0));
}
exports.transactionBytes = transactionBytes;
// blank output for calcualting change fee
exports.BLANK_OUTPUT = exports.TX_OUTPUT_BASE + exports.TX_OUTPUT_PUBKEYHASH;
function sumValues(range) {
return range.reduce((prev, v) => prev + (v.value || 0), 0);
}
exports.sumValues = sumValues;
function finalize(inputs, outputs, feeRate, changeAddress, changeOutput) {
let changeFee = exports.TX_OUTPUT_BASE + exports.TX_OUTPUT_PUBKEYHASH;
if (changeAddress) {
changeFee = outputBytes({ address: changeAddress });
}
const bytesAccum = transactionBytes(inputs, outputs);
const feeAfterExtraOutput = feeRate * (bytesAccum + changeFee);
const remainderAfterExtraOutput = sumValues(inputs) - (sumValues(outputs) + feeAfterExtraOutput);
if (changeOutput) {
// is it worth a change output?
if (remainderAfterExtraOutput > dustThreshold(feeRate)) {
outputs = outputs.concat({
address: changeAddress,
value: remainderAfterExtraOutput,
});
}
}
var fee = sumValues(inputs) - sumValues(outputs);
if (!isFinite(fee))
return { fee: feeRate * bytesAccum };
const txFee = transactionBytes(inputs, outputs) * feeRate;
return {
inputs,
outputs,
fee,
txFee,
};
}
exports.finalize = finalize;