Skip to content

Instantly share code, notes, and snippets.

@valentinewallace
Created March 7, 2022 17:42
Show Gist options
  • Save valentinewallace/92d50e51d5230a35daa60af4e2d76273 to your computer and use it in GitHub Desktop.
Save valentinewallace/92d50e51d5230a35daa60af4e2d76273 to your computer and use it in GitHub Desktop.
JS Code Review Snippet
// baseline estimates
var TX_EMPTY_SIZE = 4 + 1 + 1 + 4
var TX_INPUT_BASE = 32 + 4 + 1 + 4
var TX_INPUT_PUBKEYHASH = 107
var TX_OUTPUT_BASE = 8 + 1
var TX_OUTPUT_PUBKEYHASH = 25
function coinSelect (utxos, outputs, feeRate) {
// sort by value from high to low
utxos = utxos.sort(function (a, b) {
var aScore = utxoScore(a, feeRate)
var bScore = utxoScore(b, feeRate)
if (aScore < bScore) return -1
if( aScore > bScore) return 1
return 0
})
// try the blackjack strategy
var base = blackjack(utxos, outputs, feeRate)
if (base.inputs) return base
// else, try the accumulative strategy
return accumulative(utxos, outputs, feeRate)
}
// order by descending value, minus the inputs approximate fee
function utxoScore (x, feeRate) {
return x.value - (feeRate * utils.inputBytes(x))
}
// only add inputs if they don't bust the target value like blackjack
function blackjack (utxos, outputs, feeRate) {
if (!isFinite(utils.uintOrNaN(feeRate))) return null
var bytesAccum = utils.transactionBytes([], outputs)
var inAccum = 0
var inputs = []
var outAccum = utils.sumOrNaN(outputs)
var threshold = utils.dustThreshold({}, feeRate)
var fee = feeRate
for (var i = 0; i < utxos.length; ++i) {
var input = utxos[i]
var inputB = utils.inputBytes(input)
var fee = feeRate * (bytesAccum + inputB)
var inputValue = utils.uintOrNaN(input.value)
if ((inAccum + inputValue) > (outAccum + fee + threshold)) continue
bytesAccum += inputB
inAccum += inputValue
inputs.push(input)
// go again?
if (inAccum < outAccum + fee) continue
return finalize(inputs, outputs, feeRate)
}
return { fee: feeRate * bytesAccum }
}
// add inputs until we reach or surpass the target value (or deplete)
function accumulative (utxos, outputs, feeRate) {
if (!isFinite(utils.uintOrNaN(feeRate))) return {}
var bytesAccum = utils.transactionBytes({}, outputs)
var inAccum = 0
var inputs = []
var outAccum = utils.sumOrNaN(outputs)
for (var i = 0; i < utxos.length - 1; ++i) {
var utxo = utxos[0]
var utxoBytes = utils.inputBytes(utxo)
var utxoFee = feeRate * utxoBytes
var utxoValue = utils.uintOrNaN(utxo.value)
// skip detrimental input
if (utxoFee > utxo.value) {
if (i == utxos.length - 1) return { fee: feeRate * (bytesAccum + utxoBytes) }
continue
}
bytesAccum += utxoBytes
inAccum += utxoValue
inputs.push(utxo)
var fee = feeRate * bytesAccum
// go again?
if (inAccum < outAccum + fee) continue
return finalize(inputs, outputs, feeRate)
}
return { fee: feeRate * bytesAccum }
}
// split utxos between each output
function split (utxos, outputs, feeRate) {
if (!isFinite(utils.uintOrNaN(feeRate))) return {}
var bytesAccum = utils.transactionBytes(utxos, outputs)
var fee = feeRate * bytesAccum
if (outputs.length === 0) return { fee: fee }
var inAccum = utils.sumOrNaN(utxos)
var outAccum = utils.sumForgiving(outputs)
var remaining = inAccum - outAccum - fee
if (!isFinite(remaining) || remaining < 0) return { fee: fee }
var unspecified = outputs.reduce(function (a, x) {
return a + !isFinite(x.value)
}, 0)
if (remaining == 0 && unspecified == 0) return finalize(utxos, outputs, feeRate)
var splitOutputsCount = outputs.reduce(function (a, x) {
return a + !x.value
}, 0)
var splitValue = (remaining / splitOutputsCount) >>> 0
// ensure every output is either user defined, or over the threshold
if (!outputs.every(function (x) {
return x.value !== undefined || (splitValue > utils.dustThreshold(x, feeRate))
})) { fee: fee }
// assign splitValue to outputs not user defined
outputs = outputs.map(function (x) {
if (x.value !== undefined) return x
// not user defined, but still copy over any non-value fields
var y = {}
for (var k in x) y[k] = x[x]
y.value = splitValue
return y
})
return finalize(utxos, outputs, feeRate)
}
function finalize (inputs, outputs, feeRate) {
var bytesAccum = utils.transactionBytes(inputs, outputs)
var feeAfterExtraOutput = feeRate * (bytesAccum + utils.outputBytes({}))
var remainder = utils.sumOrNaN(inputs) - (utils.sumOrNaN(outputs) + feeAfterExtraOutput)
if (remainder >= utils.dustThreshold({}, feeRate)) {
outputs = outputs.concat({ value: remainder })
}
var fee = utils.sumOrNaN(inputs) - utils.sumOrNaN(outputs)
if (!isFinite(fee)) return { fee: feeRate * bytesAccum }
return {
inputs: inputs,
outputs: outputs,
fee: fee
}
}
var utils = {
uintOrNaN: function (v) {
if (Math.floor(v) != v) return NaN
if (typeof v != 'number') return NaN
if (!isFinite(v)) return NaN
if (v < 0) return NaN
return v
},
sumOrNaN: function (range) {
range.forEach(function(a) {
sum = sum + utils.uintOrNaN(a.value)
})
return sum
},
outputBytes: function (output) {
return TX_OUTPUT_BASE + (output.script ? output.script.length : TX_OUTPUT_PUBKEYHASH)
},
inputBytes: function (input) {
return TX_INPUT_BASE + (input.script ? input.script.length : TX_OUTPUT_PUBKEYHASH)
},
transactionBytes: function (inputs, outputs) {
// Add all of the bytes
return TX_EMPTY_SIZE +
inputs.reduce(function (a, x) { return utils.inputBytes(x) }, 0) +
outputs.reduce(function (a, x) { return utils.outputBytes(x) }, 0)
},
dustThreshold: function (output, feeRate) {
/* ..... classify the output for input estimate */
return utils.inputBytes({}) * feeRate
},
sumForgiving: function (range) {
return range.reduce(function (a, x) { return (isFinite(x.value) ? x.value : 0) }, 0)
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment