diff --git a/accumulative.js b/accumulative.js index 42fd28f..c58dd44 100644 --- a/accumulative.js +++ b/accumulative.js @@ -1,8 +1,10 @@ var utils = require('./utils') - +var processOptions = require('./defaultOpts') +var processOptionsFunc = processOptions.processOptions // add inputs until we reach or surpass the target value (or deplete) // worst-case: O(n) -module.exports = function accumulative (utxos, outputs, feeRate) { +module.exports = function accumulative (utxos, outputs, feeRate, options) { + options = processOptionsFunc(options) if (!isFinite(utils.uintOrNaN(feeRate))) return {} var bytesAccum = utils.transactionBytes([], outputs) @@ -31,7 +33,7 @@ module.exports = function accumulative (utxos, outputs, feeRate) { // go again? if (inAccum < outAccum + fee) continue - return utils.finalize(inputs, outputs, feeRate) + return utils.finalize(inputs, outputs, feeRate, options) } return { fee: feeRate * bytesAccum } diff --git a/blackjack.js b/blackjack.js index 316568b..90d265c 100644 --- a/blackjack.js +++ b/blackjack.js @@ -1,8 +1,10 @@ var utils = require('./utils') - +var processOptions = require('./defaultOpts') +var processOptionsFunc = processOptions.processOptions // only add inputs if they don't bust the target value (aka, exact match) // worst-case: O(n) -module.exports = function blackjack (utxos, outputs, feeRate) { +module.exports = function blackjack (utxos, outputs, feeRate, options) { + options = processOptionsFunc() if (!isFinite(utils.uintOrNaN(feeRate))) return {} var bytesAccum = utils.transactionBytes([], outputs) @@ -10,7 +12,7 @@ module.exports = function blackjack (utxos, outputs, feeRate) { var inAccum = 0 var inputs = [] var outAccum = utils.sumOrNaN(outputs) - var threshold = utils.dustThreshold({}, feeRate) + var threshold = utils.dustThreshold(feeRate, options.changeInputLengthEstimate) for (var i = 0; i < utxos.length; ++i) { var input = utxos[i] @@ -28,7 +30,7 @@ module.exports = function blackjack (utxos, outputs, feeRate) { // go again? if (inAccum < outAccum + fee) continue - return utils.finalize(inputs, outputs, feeRate) + return utils.finalize(inputs, outputs, feeRate, options) } return { fee: feeRate * bytesAccum } diff --git a/break.js b/break.js index 3795aa1..ac4f67f 100644 --- a/break.js +++ b/break.js @@ -1,7 +1,9 @@ var utils = require('./utils') - -// break utxos into the maximum number of output possible -module.exports = function broken (utxos, output, feeRate) { +var processOptions = require('./defaultOpts') +var processOptionsFunc = processOptions.processOptions +// break utxos into the maximum number of 'output' possible +module.exports = function broken (utxos, output, feeRate, options) { + options = processOptionsFunc(options) if (!isFinite(utils.uintOrNaN(feeRate))) return {} var bytesAccum = utils.transactionBytes(utxos, []) @@ -29,6 +31,5 @@ module.exports = function broken (utxos, output, feeRate) { outAccum += value outputs.push(output) } - - return utils.finalize(utxos, outputs, feeRate) + return utils.finalize(utxos, outputs, feeRate, options) } diff --git a/defaultOpts.js b/defaultOpts.js new file mode 100644 index 0000000..9dc39fd --- /dev/null +++ b/defaultOpts.js @@ -0,0 +1,11 @@ +const defaultOpts = { + changeInputLengthEstimate: 107, + changeOutputLength: 25 +} + +function processOptions (options) { + const mergerdOptions = Object.assign(defaultOpts, options) + return mergerdOptions +} + +exports.processOptions = processOptions diff --git a/index.js b/index.js index 19aa484..40aeb9b 100644 --- a/index.js +++ b/index.js @@ -1,21 +1,23 @@ var accumulative = require('./accumulative') var blackjack = require('./blackjack') var utils = require('./utils') - +var processOptions = require('./defaultOpts') +var processOptionsFunc = processOptions.processOptions // order by descending value, minus the inputs approximate fee function utxoScore (x, feeRate) { return x.value - (feeRate * utils.inputBytes(x)) } -module.exports = function coinSelect (utxos, outputs, feeRate) { +module.exports = function coinSelect (utxos, outputs, feeRate, options) { + options = processOptionsFunc() utxos = utxos.concat().sort(function (a, b) { return utxoScore(b, feeRate) - utxoScore(a, feeRate) }) // attempt to use the blackjack strategy first (no change output) - var base = blackjack(utxos, outputs, feeRate) + var base = blackjack(utxos, outputs, feeRate, options) if (base.inputs) return base // else, try the accumulative strategy - return accumulative(utxos, outputs, feeRate) + return accumulative(utxos, outputs, feeRate, options) } diff --git a/split.js b/split.js index 4d3ce5d..da197e3 100644 --- a/split.js +++ b/split.js @@ -1,7 +1,9 @@ var utils = require('./utils') - +var processOptions = require('./defaultOpts') +var processOptionsFunc = processOptions.processOptions // split utxos between each output, ignores outputs with .value defined -module.exports = function split (utxos, outputs, feeRate) { +module.exports = function split (utxos, outputs, feeRate, options) { + options = processOptionsFunc() if (!isFinite(utils.uintOrNaN(feeRate))) return {} var bytesAccum = utils.transactionBytes(utxos, outputs) @@ -17,7 +19,7 @@ module.exports = function split (utxos, outputs, feeRate) { return a + !isFinite(x.value) }, 0) - if (remaining === 0 && unspecified === 0) return utils.finalize(utxos, outputs, feeRate) + if (remaining === 0 && unspecified === 0) return utils.finalize(utxos, outputs, feeRate, options.changeInputLengthEstimate, options.changeOutputLength) var splitOutputsCount = outputs.reduce(function (a, x) { return a + !x.value @@ -26,7 +28,7 @@ module.exports = function split (utxos, outputs, feeRate) { // 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)) + return x.value !== undefined || (splitValue > utils.dustThreshold(feeRate, options.changeInputLengthEstimate)) })) return { fee: fee } // assign splitValue to outputs not user defined @@ -40,5 +42,5 @@ module.exports = function split (utxos, outputs, feeRate) { return y }) - return utils.finalize(utxos, outputs, feeRate) + return utils.finalize(utxos, outputs, feeRate, options) } diff --git a/stats/index.js b/stats/index.js index 6885d5f..af9d91a 100644 --- a/stats/index.js +++ b/stats/index.js @@ -1,34 +1,81 @@ -let Simulation = require('./simulation') -let modules = require('./strategies') -let min = 14226 // 0.1 USD -let max = 142251558 // 1000 USD -let feeRate = 56 * 100 -let results = [] +const Simulation = require('./simulation') +const modules = require('./strategies') +const min = 14226 // 0.1 USD +const max = 142251558 // 1000 USD +const feeRate = 56 * 100 +const results = [] + +// switch between two modes +// true - failed payments are discarded +// false - failed payments are queued until there is enough balance +const discardFailed = false +const walletType = 'p2pkh' // set to either p2pkh or p2sh + +// data from blockchaing from ~august 2015 - august 2017 +// see https://gist.github.com/runn1ng/8e2881e5bb44e01748e46b3d1c549038 +// these are 2-of-3 lengths, most common p2sh (66%), percs changed so they add to 100 +const scripthashScriptLengthData = { + prob: 0.16, + inLengthPercs: { + 252: 25.29, + 253: 49.77, + 254: 24.94 + }, + outLength: 23 +} + +// these are compressed key length (90% of p2pkh inputs) +// percs changed so they add to 100 +const pubkeyhashScriptLengthData = { + prob: 0.84, + inLengthPercs: { + 105: 0.4, + 106: 50.7, + 107: 48.81, + 108: 0.09 + }, + outLength: 25 +} + +const selectedData = walletType === 'p2pkh' ? pubkeyhashScriptLengthData : scripthashScriptLengthData +const inLengthProbs = selectedData.inLengthPercs + +const outLengthProbs = {}; +[scripthashScriptLengthData, pubkeyhashScriptLengthData].forEach(({ prob, outLength }) => { + outLengthProbs[outLength] = prob +}) // n samples for (var j = 0; j < 100; ++j) { if (j % 200 === 0) console.log('Iteration', j) - let stages = [] + const stages = [] for (var i = 1; i < 4; ++i) { - let utxos = Simulation.generateTxos(20 / i, min, max) - let txos = Simulation.generateTxos(80 / i, min, max / 3) - + const utxos = Simulation.generateTxos(20 / i, min, max, inLengthProbs) + const txos = Simulation.generateTxos(80 / i, min, max / 3, outLengthProbs) stages.push({ utxos, txos }) } // for each strategy for (var name in modules) { - let f = modules[name] - let simulation = new Simulation(name, f, feeRate) + const f = modules[name] + const simulation = new Simulation(name, f, feeRate) stages.forEach((stage) => { // supplement our UTXOs - stage.utxos.forEach(x => simulation.addUTXO(x)) + stage.utxos.forEach(x => { + simulation.addUTXO(x) + + // if discardFailed == true, this should do nothing + simulation.run(discardFailed) + }) // now, run stage.txos.length transactions - stage.txos.forEach((txo) => simulation.runQueued([txo])) + stage.txos.forEach((txo) => { + simulation.plan([txo]) + simulation.run(discardFailed) + }) }) simulation.finish() @@ -37,16 +84,16 @@ for (var j = 0; j < 100; ++j) { } function merge (results) { - let resultMap = {} + const resultMap = {} results.forEach(({ stats }) => { - let result = resultMap[stats.name] + const result = resultMap[stats.name] if (result) { result.inputs += stats.inputs result.outputs += stats.outputs result.transactions += stats.transactions - result.failed += stats.failed + result.plannedTransactions += stats.plannedTransactions result.fees += stats.fees result.bytes += stats.bytes result.utxos += stats.utxos @@ -76,8 +123,8 @@ merge(results).sort((a, b) => { // top 20 only }).slice(0, 20).forEach((x, i) => { - let { stats } = x - let DNF = stats.failed / (stats.transactions + stats.failed) + const { stats } = x + const DNF = 1 - stats.transactions / stats.plannedTransactions console.log( pad(i), diff --git a/stats/simulation.js b/stats/simulation.js index 35ebd1b..1658250 100644 --- a/stats/simulation.js +++ b/stats/simulation.js @@ -1,3 +1,5 @@ +var weighted = require('weighted') + function rayleight (a, b) { return a + b * Math.sqrt(-Math.log(uniform(0, 1))) } @@ -18,27 +20,26 @@ function Simulation (name, algorithm, feeRate) { this.utxoMap = {} this.stats = { name, + plannedTransactions: 0, transactions: 0, inputs: 0, outputs: 0, fees: 0, - bytes: 0, - failed: 0 + bytes: 0 } - this.queue = [] + this.planned = [] // used for tracking UTXOs (w/o transaction ids) this.k = 0 } -Simulation.generateTxos = function (n, min, max) { - let txos = [] +Simulation.generateTxos = function (n, min, max, scriptSizes) { + const txos = [] for (let i = 0; i < n; ++i) { - let v = rayleight(min, max) >>> 0 + const v = rayleight(min, max) >>> 0 - let s = 106 - if (Math.random() > 0.9) s = 300 + const s = parseInt(weighted.select(scriptSizes)) txos.push({ address: randomAddress(), @@ -51,15 +52,12 @@ Simulation.generateTxos = function (n, min, max) { return txos } -Simulation.prototype.addUTXO = function (utxo, change) { - let k = this.k + 1 +Simulation.prototype.addUTXO = function (utxo) { + const k = this.k + 1 if (this.utxoMap[k] !== undefined) throw new Error('Bad UTXO') this.utxoMap[k] = Object.assign({}, utxo, { id: k }) this.k = k - if (!change) { - this.tryQueue() - } } Simulation.prototype.useUTXO = function (utxo) { @@ -69,49 +67,37 @@ Simulation.prototype.useUTXO = function (utxo) { } Simulation.prototype.getUTXOs = function () { - let utxos = [] - for (let k in this.utxoMap) utxos.push(this.utxoMap[k]) + const utxos = [] + for (const k in this.utxoMap) utxos.push(this.utxoMap[k]) return utxos } -Simulation.prototype.runQueued = function (outputs) { - this.queue.push(outputs) - this.tryQueue() +Simulation.prototype.plan = function (outputs) { + this.stats.plannedTransactions += 1 + this.planned.push(outputs) } -Simulation.prototype.tryQueue = function () { - while (this.queue.length > 0) { - let outputs = this.queue[0] - let utxos = this.getUTXOs() +Simulation.prototype.run = function (discardFailed) { + while (this.planned.length > 0) { + const outputs = this.planned[0] + const utxos = this.getUTXOs() - let { inputs, outputs: outputs2, fee } = this.algorithm(utxos, outputs, this.feeRate) + const { inputs, outputs: outputs2, fee } = this.algorithm(utxos, outputs, this.feeRate) if (!inputs) { + if (discardFailed) { + this.planned.shift() + } return } - this.queue.shift() - - this.useAlgorithmResult(inputs, outputs2, fee) - } -} - -Simulation.prototype.run = function (outputs) { - let utxos = this.getUTXOs() - - let { inputs, outputs: outputs2, fee } = this.algorithm(utxos, outputs, this.feeRate) + this.planned.shift() - if (!inputs) { - this.stats.failed += 1 - return + this.useResult(inputs, outputs2, fee) } - - this.useAlgorithmResult(inputs, outputs2, fee) - - return true } -Simulation.prototype.useAlgorithmResult = function (inputs, outputs, fee) { +Simulation.prototype.useResult = function (inputs, outputs, fee) { this.stats.transactions += 1 this.stats.inputs += inputs.length this.stats.outputs += outputs.length @@ -130,14 +116,14 @@ Simulation.prototype.useAlgorithmResult = function (inputs, outputs, fee) { outputs.filter(x => x.script === undefined).forEach((x) => { // assign it a random address x.address = randomAddress() - this.addUTXO(x, true) + this.addUTXO(x) }) } Simulation.prototype.finish = function () { - let utxos = this.getUTXOs() + const utxos = this.getUTXOs() this.stats.utxos = utxos.length - let costToEmpty = utils.transactionBytes(utxos, []) * this.feeRate // output cost is negligible + const costToEmpty = utils.transactionBytes(utxos, []) * this.feeRate // output cost is negligible this.stats.totalCost = this.stats.fees + costToEmpty } diff --git a/stats/strategies.js b/stats/strategies.js index bf86280..4fcd4c3 100644 --- a/stats/strategies.js +++ b/stats/strategies.js @@ -1,16 +1,16 @@ -let accumulative = require('../accumulative') -let blackjack = require('../blackjack') -let shuffle = require('fisher-yates') -let shuffleInplace = require('fisher-yates/inplace') -let coinSelect = require('../') -let utils = require('../utils') +const accumulative = require('../accumulative') +const blackjack = require('../blackjack') +const shuffle = require('fisher-yates') +const shuffleInplace = require('fisher-yates/inplace') +const coinSelect = require('../') +const utils = require('../utils') function blackmax (utxos, outputs, feeRate) { // order by ascending value utxos = utxos.concat().sort((a, b) => a.value - b.value) // attempt to use the blackjack strategy first (no change output) - let base = blackjack(utxos, outputs, feeRate) + const base = blackjack(utxos, outputs, feeRate) if (base.inputs) return base // else, try the accumulative strategy @@ -22,7 +22,7 @@ function blackmin (utxos, outputs, feeRate) { utxos = utxos.concat().sort((a, b) => b.value - a.value) // attempt to use the blackjack strategy first (no change output) - let base = blackjack(utxos, outputs, feeRate) + const base = blackjack(utxos, outputs, feeRate) if (base.inputs) return base // else, try the accumulative strategy @@ -33,7 +33,7 @@ function blackrand (utxos, outputs, feeRate) { utxos = shuffle(utxos) // attempt to use the blackjack strategy first (no change output) - let base = blackjack(utxos, outputs, feeRate) + const base = blackjack(utxos, outputs, feeRate) if (base.inputs) return base // else, try the accumulative strategy @@ -62,8 +62,8 @@ function proximal (utxos, outputs, feeRate) { const outAccum = outputs.reduce((a, x) => a + x.value, 0) utxos = utxos.concat().sort((a, b) => { - let aa = a.value - outAccum - let bb = b.value - outAccum + const aa = a.value - outAccum + const bb = b.value - outAccum return aa - bb }) @@ -80,13 +80,13 @@ function random (utxos, outputs, feeRate) { function bestof (utxos, outputs, feeRate) { let n = 100 - let utxosCopy = utxos.concat() + const utxosCopy = utxos.concat() let best = { fee: Infinity } while (n) { shuffleInplace(utxosCopy) - let result = accumulative(utxos, outputs, feeRate) + const result = accumulative(utxos, outputs, feeRate) if (result.inputs && result.fee < best.fee) { best = result } @@ -102,7 +102,7 @@ function utxoScore (x, feeRate) { } function privet (utxos, outputs, feeRate) { - let txosMap = {} + const txosMap = {} utxos.forEach((txo) => { if (!txosMap[txo.address]) { txosMap[txo.address] = [] diff --git a/test/_utils.js b/test/_utils.js index 4c3739d..bf5a6e4 100644 --- a/test/_utils.js +++ b/test/_utils.js @@ -1,17 +1,52 @@ -function expand (values, indices) { +function addScriptLength (values, scriptLength) { + return values.map(function (x) { + if (x.script === undefined) { + x.script = { length: scriptLength } + } + return x + }) +} + +function addScriptLengthToExpected (expected, inputLength, outputLength) { + var newExpected = Object.assign({}, expected) + + if (expected.inputs != null) { + newExpected.inputs = expected.inputs.map(function (input) { + var newInput = Object.assign({}, input) + if (newInput.script == null) { + newInput.script = { length: inputLength } + } + return newInput + }) + } + + if (expected.outputs != null) { + newExpected.outputs = expected.outputs.map(function (output) { + var newOutput = Object.assign({}, output) + if (newOutput.script == null) { + newOutput.script = { length: outputLength } + } + return newOutput + }) + } + + return newExpected +} + +function expand (values, indices, scriptLength) { if (indices) { - return values.map(function (x, i) { + return addScriptLength(values.map(function (x, i) { if (typeof x === 'number') return { i: i, value: x } var y = { i: i } for (var k in x) y[k] = x[k] return y - }) + }), scriptLength) } - return values.map(function (x, i) { + return addScriptLength(values.map(function (x, i) { return typeof x === 'object' ? x : { value: x } - }) + }), scriptLength) } function testValues (t, actual, expected) { @@ -35,5 +70,6 @@ function testValues (t, actual, expected) { module.exports = { expand: expand, - testValues: testValues + testValues: testValues, + addScriptLengthToExpected: addScriptLengthToExpected } diff --git a/test/accumulative.js b/test/accumulative.js index 6f6edd7..e176032 100644 --- a/test/accumulative.js +++ b/test/accumulative.js @@ -2,17 +2,25 @@ var coinAccum = require('../accumulative') var fixtures = require('./fixtures/accumulative') var tape = require('tape') var utils = require('./_utils') +var processOptions = require('../defaultOpts') +var processOptionsFunc = processOptions.processOptions fixtures.forEach(function (f) { tape(f.description, function (t) { - var inputs = utils.expand(f.inputs, true) - var outputs = utils.expand(f.outputs) + const options = processOptionsFunc() + var inputLength = options.changeInputLengthEstimate + var outputLength = options.changeOutputLength + + var inputs = utils.expand(f.inputs, true, inputLength) + var outputs = utils.expand(f.outputs, false, outputLength) + var expected = utils.addScriptLengthToExpected(f.expected, inputLength, outputLength) + var actual = coinAccum(inputs, outputs, f.feeRate) - t.same(actual, f.expected) + t.same(actual, expected) if (actual.inputs) { var feedback = coinAccum(actual.inputs, actual.outputs, f.feeRate) - t.same(feedback, f.expected) + t.same(feedback, expected) } t.end() diff --git a/test/break.js b/test/break.js index 9748b6c..778e1e1 100644 --- a/test/break.js +++ b/test/break.js @@ -2,14 +2,22 @@ var coinBreak = require('../break') var fixtures = require('./fixtures/break') var tape = require('tape') var utils = require('./_utils') +var processOptions = require('../defaultOpts') +var processOptionsFunc = processOptions.processOptions fixtures.forEach(function (f) { tape(f.description, function (t) { - var finputs = utils.expand(f.inputs) - var foutputs = utils.expand([f.output]) - var actual = coinBreak(finputs, foutputs[0], f.feeRate) + const options = processOptionsFunc() + var inputLength = options.changeInputLengthEstimate + var outputLength = options.changeOutputLength - t.same(actual, f.expected) + var inputs = utils.expand(f.inputs, false, inputLength) + var outputs = utils.expand([f.output], false, outputLength) + var expected = utils.addScriptLengthToExpected(f.expected, inputLength, outputLength) + + var actual = coinBreak(inputs, outputs[0], f.feeRate) + + t.same(actual, expected) t.end() }) }) diff --git a/test/fixtures/index.json b/test/fixtures/index.json index 9ebcdbf..cb2a720 100644 --- a/test/fixtures/index.json +++ b/test/fixtures/index.json @@ -806,4 +806,3 @@ "expected": {} } ] - diff --git a/test/index.js b/test/index.js index 3554363..da727c8 100644 --- a/test/index.js +++ b/test/index.js @@ -2,17 +2,23 @@ var coinSelect = require('../') var fixtures = require('./fixtures') var tape = require('tape') var utils = require('./_utils') +var processOptions = require('../defaultOpts') +var processOptionsFunc = processOptions.processOptions fixtures.forEach(function (f) { tape(f.description, function (t) { - var inputs = utils.expand(f.inputs, true) - var outputs = utils.expand(f.outputs) + const options = processOptionsFunc() + var inputLength = options.changeInputLengthEstimate + var outputLength = options.changeOutputLength + var inputs = utils.expand(f.inputs, true, inputLength) + var outputs = utils.expand(f.outputs, false, outputLength) + var expected = utils.addScriptLengthToExpected(f.expected, inputLength, outputLength) var actual = coinSelect(inputs, outputs, f.feeRate) - t.same(actual, f.expected) + t.same(actual, expected) if (actual.inputs) { var feedback = coinSelect(actual.inputs, actual.outputs, f.feeRate) - t.same(feedback, f.expected) + t.same(feedback, expected) } t.end() diff --git a/test/split.js b/test/split.js index 0cd8c4b..fbfe21d 100644 --- a/test/split.js +++ b/test/split.js @@ -2,17 +2,25 @@ var coinSplit = require('../split') var fixtures = require('./fixtures/split') var tape = require('tape') var utils = require('./_utils') +var processOptions = require('../defaultOpts') +var processOptionsFunc = processOptions.processOptions fixtures.forEach(function (f) { tape(f.description, function (t) { - var finputs = utils.expand(f.inputs) - var foutputs = f.outputs.concat() - var actual = coinSplit(finputs, foutputs, f.feeRate) + const options = processOptionsFunc() + var inputLength = options.changeInputLengthEstimate + var outputLength = options.changeOutputLength - t.same(actual, f.expected) + var inputs = utils.expand(f.inputs, false, inputLength) + var outputs = utils.expand(f.outputs.concat(), false, outputLength) + var expected = utils.addScriptLengthToExpected(f.expected, inputLength, outputLength) + + var actual = coinSplit(inputs, outputs, f.feeRate) + + t.same(actual, expected) if (actual.inputs) { - var feedback = coinSplit(finputs, actual.outputs, f.feeRate) - t.same(feedback, f.expected) + var feedback = coinSplit(inputs, actual.outputs, f.feeRate) + t.same(feedback, expected) } t.end() diff --git a/utils.js b/utils.js index 687fe66..7f037ee 100644 --- a/utils.js +++ b/utils.js @@ -1,21 +1,18 @@ // baseline estimates, used to improve performance 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 inputBytes (input) { - return TX_INPUT_BASE + (input.script ? input.script.length : TX_INPUT_PUBKEYHASH) + return TX_INPUT_BASE + input.script.length } function outputBytes (output) { - return TX_OUTPUT_BASE + (output.script ? output.script.length : TX_OUTPUT_PUBKEYHASH) + return TX_OUTPUT_BASE + output.script.length } -function dustThreshold (output, feeRate) { - /* ... classify the output for input estimate */ - return inputBytes({}) * feeRate +function dustThreshold (feeRate, inputLenghtEstimate) { + return inputBytes({ script: { length: inputLenghtEstimate } }) * feeRate } function transactionBytes (inputs, outputs) { @@ -40,16 +37,15 @@ function sumOrNaN (range) { return range.reduce(function (a, x) { return a + uintOrNaN(x.value) }, 0) } -var BLANK_OUTPUT = outputBytes({}) - -function finalize (inputs, outputs, feeRate) { +function finalize (inputs, outputs, feeRate, options) { var bytesAccum = transactionBytes(inputs, outputs) - var feeAfterExtraOutput = feeRate * (bytesAccum + BLANK_OUTPUT) + var blankOutputBytes = outputBytes({ script: { length: options.changeOutputLength } }) + var feeAfterExtraOutput = feeRate * (bytesAccum + blankOutputBytes) var remainderAfterExtraOutput = sumOrNaN(inputs) - (sumOrNaN(outputs) + feeAfterExtraOutput) // is it worth a change output? - if (remainderAfterExtraOutput > dustThreshold({}, feeRate)) { - outputs = outputs.concat({ value: remainderAfterExtraOutput }) + if (remainderAfterExtraOutput > dustThreshold(feeRate, options.changeInputLengthEstimate)) { + outputs = outputs.concat({ value: remainderAfterExtraOutput, script: { length: options.changeOutputLength } }) } var fee = sumOrNaN(inputs) - sumOrNaN(outputs)