@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
159 lines (158 loc) • 5.79 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.utils = exports.DUST_THRESHOLDS = void 0;
const Logger_1 = require("../../utils/Logger");
const logger = (0, Logger_1.getLogger)("CoinSelect: ");
// baseline estimates, used to improve performance
const TX_EMPTY_SIZE = 4 + 1 + 1 + 4;
const TX_INPUT_BASE = 32 + 4 + 1 + 4;
const WITNESS_OVERHEAD = 2 / 4;
const P2WPKH_WITNESS = (1 + 1 + 72 + 1 + 33) / 4;
const P2TR_WITNESS = (1 + 1 + 65) / 4;
const TX_INPUT_PUBKEYHASH = 107;
const TX_INPUT_P2SH_P2WPKH = 23 + P2WPKH_WITNESS + 1;
const TX_INPUT_P2WPKH = 0 + P2WPKH_WITNESS;
const TX_INPUT_P2WSH = 0 + (1 + 1 + 64) / 4;
const TX_INPUT_P2TR = 0 + P2TR_WITNESS;
const TX_OUTPUT_BASE = 8 + 1;
const TX_OUTPUT_PUBKEYHASH = 25;
const TX_OUTPUT_P2SH_P2WPKH = 23;
const TX_OUTPUT_P2WPKH = 22;
const TX_OUTPUT_P2WSH = 34;
const TX_OUTPUT_P2TR = 34;
const INPUT_BYTES = {
"p2sh-p2wpkh": TX_INPUT_P2SH_P2WPKH,
"p2wpkh": TX_INPUT_P2WPKH,
"p2tr": TX_INPUT_P2TR,
"p2pkh": TX_INPUT_PUBKEYHASH,
"p2wsh": TX_INPUT_P2WSH
};
function inputBytes(input) {
if (input.script == null && input.type == null)
throw new Error("Needs either script or type defined!");
return TX_INPUT_BASE + (input.script ? input.script.length : INPUT_BYTES[input.type]);
}
const OUTPUT_BYTES = {
"p2sh-p2wpkh": TX_OUTPUT_P2SH_P2WPKH,
"p2wpkh": TX_OUTPUT_P2WPKH,
"p2tr": TX_OUTPUT_P2TR,
"p2pkh": TX_OUTPUT_PUBKEYHASH,
"p2wsh": TX_OUTPUT_P2WSH
};
function outputBytes(output) {
if (output.script == null && output.type == null)
throw new Error("Needs either script or type defined!");
return TX_OUTPUT_BASE + (output.script ? output.script.length : OUTPUT_BYTES[output.type]);
}
exports.DUST_THRESHOLDS = {
"p2sh-p2wpkh": 540,
"p2wpkh": 294,
"p2tr": 330,
"p2pkh": 546,
"p2wsh": 330
};
function dustThreshold(output) {
return exports.DUST_THRESHOLDS[output.type];
}
function transactionBytes(inputs, outputs, changeType) {
let size = TX_EMPTY_SIZE;
let isSegwit = false;
if (changeType != null && changeType !== "p2pkh") {
size += WITNESS_OVERHEAD;
isSegwit = true;
}
for (let input of inputs) {
if (!isSegwit && (input.type !== "p2pkh")) {
isSegwit = true;
size += WITNESS_OVERHEAD;
}
size += inputBytes(input);
}
for (let output of outputs) {
size += outputBytes(output);
}
return Math.ceil(size);
}
function numberOrNaN(v) {
if (typeof v !== 'number')
return NaN;
if (!isFinite(v))
return NaN;
if (v < 0)
return NaN;
return v;
}
function uintOrNaN(v) {
if (typeof v !== 'number')
return NaN;
if (!isFinite(v))
return NaN;
if (Math.floor(v) !== v)
return NaN;
if (v < 0)
return NaN;
return v;
}
function sumForgiving(range) {
return range.reduce((a, x) => a + (isFinite(x.value) ? x.value : 0), 0);
}
function sumOrNaN(range) {
return range.reduce((a, x) => a + uintOrNaN(x.value), 0);
}
function finalize(inputs, outputs, feeRate, changeType, cpfpAddFee = 0) {
const bytesAccum = transactionBytes(inputs, outputs, changeType ?? undefined);
logger.debug("finalize(): Transaction bytes: ", bytesAccum);
if (changeType != null) {
const feeAfterExtraOutput = (feeRate * (bytesAccum + outputBytes({ type: changeType }))) + cpfpAddFee;
logger.debug("finalize(): TX fee after adding change output: ", feeAfterExtraOutput);
const remainderAfterExtraOutput = Math.floor(sumOrNaN(inputs) - (sumOrNaN(outputs) + feeAfterExtraOutput));
logger.debug("finalize(): Leaves change (changeType=" + changeType + ") value: ", remainderAfterExtraOutput);
// is it worth a change output?
if (remainderAfterExtraOutput >= dustThreshold({ type: changeType })) {
outputs = outputs.concat({ value: remainderAfterExtraOutput, type: changeType });
}
}
const fee = sumOrNaN(inputs) - sumOrNaN(outputs);
logger.debug("finalize(): Re-calculated total fee: ", fee);
if (!isFinite(fee) || fee < 0)
return { fee: (feeRate * bytesAccum) + cpfpAddFee };
let txVSize = exports.utils.transactionBytes(inputs, outputs);
let txFee = fee;
const cpfpSortedInputs = [...inputs].sort((a, b) => (b.cpfp?.txEffectiveFeeRate ?? 0) - (a.cpfp?.txEffectiveFeeRate ?? 0));
cpfpSortedInputs.forEach(input => {
if (input.cpfp == null)
return;
const currentEffectiveFeeRate = txFee / txVSize;
if (currentEffectiveFeeRate > input.cpfp.txEffectiveFeeRate) {
txVSize += input.cpfp.txVsize;
txFee += input.cpfp.txVsize * input.cpfp.txEffectiveFeeRate;
}
});
return {
inputs: inputs,
outputs: outputs,
effectiveFeeRate: txFee / txVSize,
fee: fee
};
}
function isDetrimentalInput(feeRate, utxo) {
const utxoBytes = exports.utils.inputBytes(utxo);
const utxoFee = feeRate * utxoBytes;
let cpfpFee = 0;
if (utxo.cpfp != null && utxo.cpfp.txEffectiveFeeRate < feeRate)
cpfpFee = Math.ceil(utxo.cpfp.txVsize * (feeRate - utxo.cpfp.txEffectiveFeeRate));
// skip detrimental input
return utxoFee + cpfpFee > utxo.value;
}
exports.utils = {
dustThreshold: dustThreshold,
finalize: finalize,
inputBytes: inputBytes,
outputBytes: outputBytes,
sumOrNaN: sumOrNaN,
sumForgiving: sumForgiving,
transactionBytes: transactionBytes,
uintOrNaN: uintOrNaN,
numberOrNaN: numberOrNaN,
isDetrimentalInput
};