├── .github └── workflows │ └── main_ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── accumulative.js ├── blackjack.js ├── break.js ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json ├── split.js ├── stats ├── index.js ├── package.json ├── simulation.js └── strategies.js ├── test ├── _utils.js ├── accumulative.js ├── break.js ├── fixtures │ ├── accumulative.json │ ├── break.json │ ├── index.json │ └── split.json ├── index.js ├── split.js └── utils.js └── utils.js /.github/workflows/main_ci.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | unit: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [14, 'lts/*'] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | steps: 20 | - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@eeb10cff27034e7acf239c5d29f62154018672fd # v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | registry-url: https://registry.npmjs.org/ 26 | cache: 'npm' 27 | - run: npm i 28 | - run: npm run unit 29 | coverage: 30 | runs-on: ubuntu-latest 31 | strategy: 32 | matrix: 33 | node-version: [14, 'lts/*'] 34 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 35 | steps: 36 | - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 37 | - name: Use Node.js ${{ matrix.node-version }} 38 | uses: actions/setup-node@eeb10cff27034e7acf239c5d29f62154018672fd # v3 39 | with: 40 | node-version: ${{ matrix.node-version }} 41 | registry-url: https://registry.npmjs.org/ 42 | cache: 'npm' 43 | - run: npm i 44 | - run: npm run coverage 45 | lint: 46 | runs-on: ubuntu-latest 47 | strategy: 48 | matrix: 49 | node-version: [14, 'lts/*'] 50 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 51 | steps: 52 | - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 53 | - name: Use Node.js ${{ matrix.node-version }} 54 | uses: actions/setup-node@eeb10cff27034e7acf239c5d29f62154018672fd # v3 55 | with: 56 | node-version: ${{ matrix.node-version }} 57 | registry-url: https://registry.npmjs.org/ 58 | cache: 'npm' 59 | - run: npm i 60 | - run: npm run standard 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | coverage 3 | .nyc_output 4 | node_modules 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Daniel Cousens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coinselect 2 | 3 | [![TRAVIS](https://secure.travis-ci.org/bitcoinjs/coinselect.png)](http://travis-ci.org/bitcoinjs/coinselect) 4 | [![NPM](http://img.shields.io/npm/v/coinselect.svg)](https://www.npmjs.org/package/coinselect) 5 | 6 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 7 | 8 | An unspent transaction output (UTXO) selection module for bitcoin. 9 | 10 | **WARNING:** Value units are in `satoshi`s, **not** Bitcoin. 11 | 12 | 13 | ## Algorithms 14 | Module | Algorithm | Re-orders UTXOs? 15 | -|-|- 16 | `require('coinselect')` | Blackjack, with Accumulative fallback | By Descending Value 17 | `require('coinselect/accumulative')` | Accumulative - accumulates inputs until the target value (+fees) is reached, skipping detrimental inputs | - 18 | `require('coinselect/blackjack')` | Blackjack - accumulates inputs until the target value (+fees) is matched, does not accumulate inputs that go over the target value (within a threshold) | - 19 | `require('coinselect/break')` | Break - breaks the input values into equal denominations of `output` (as provided) | - 20 | `require('coinselect/split')` | Split - splits the input values evenly between all `outputs`, any provided `output` with `.value` remains unchanged | - 21 | 22 | 23 | **Note:** Each algorithm will add a change output if the `input - output - fee` value difference is over a dust threshold. 24 | This is calculated independently by `utils.finalize`, irrespective of the algorithm chosen, for the purposes of safety. 25 | 26 | **Pro-tip:** if you want to send-all inputs to an output address, `coinselect/split` with a partial output (`.address` defined, no `.value`) can be used to send-all, while leaving an appropriate amount for the `fee`. 27 | 28 | ## Example 29 | 30 | ``` javascript 31 | let coinSelect = require('coinselect') 32 | let feeRate = 55 // satoshis per byte 33 | let utxos = [ 34 | ..., 35 | { 36 | txId: '...', 37 | vout: 0, 38 | ..., 39 | value: 10000, 40 | // For use with PSBT: 41 | // not needed for coinSelect, but will be passed on to inputs later 42 | nonWitnessUtxo: Buffer.from('...full raw hex of txId tx...', 'hex'), 43 | // OR 44 | // if your utxo is a segwit output, you can use witnessUtxo instead 45 | witnessUtxo: { 46 | script: Buffer.from('... scriptPubkey hex...', 'hex'), 47 | value: 10000 // 0.0001 BTC and is the exact same as the value above 48 | } 49 | } 50 | ] 51 | let targets = [ 52 | ..., 53 | { 54 | address: '1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm', 55 | value: 5000 56 | } 57 | ] 58 | 59 | // ... 60 | let { inputs, outputs, fee } = coinSelect(utxos, targets, feeRate) 61 | 62 | // the accumulated fee is always returned for analysis 63 | console.log(fee) 64 | 65 | // .inputs and .outputs will be undefined if no solution was found 66 | if (!inputs || !outputs) return 67 | 68 | let psbt = new bitcoin.Psbt() 69 | 70 | inputs.forEach(input => 71 | psbt.addInput({ 72 | hash: input.txId, 73 | index: input.vout, 74 | nonWitnessUtxo: input.nonWitnessUtxo, 75 | // OR (not both) 76 | witnessUtxo: input.witnessUtxo, 77 | }) 78 | ) 79 | outputs.forEach(output => { 80 | // watch out, outputs may have been added that you need to provide 81 | // an output address/script for 82 | if (!output.address) { 83 | output.address = wallet.getChangeAddress() 84 | wallet.nextChangeAddress() 85 | } 86 | 87 | psbt.addOutput({ 88 | address: output.address, 89 | value: output.value, 90 | }) 91 | }) 92 | ``` 93 | 94 | 95 | ## License [MIT](LICENSE) 96 | -------------------------------------------------------------------------------- /accumulative.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | 3 | // add inputs until we reach or surpass the target value (or deplete) 4 | // worst-case: O(n) 5 | module.exports = function accumulative (utxos, outputs, feeRate) { 6 | if (!isFinite(utils.uintOrNaN(feeRate))) return {} 7 | var bytesAccum = utils.transactionBytes([], outputs) 8 | 9 | var inAccum = 0 10 | var inputs = [] 11 | var outAccum = utils.sumOrNaN(outputs) 12 | 13 | for (var i = 0; i < utxos.length; ++i) { 14 | var utxo = utxos[i] 15 | var utxoBytes = utils.inputBytes(utxo) 16 | var utxoFee = feeRate * utxoBytes 17 | var utxoValue = utils.uintOrNaN(utxo.value) 18 | 19 | // skip detrimental input 20 | if (utxoFee > utxo.value) { 21 | if (i === utxos.length - 1) return { fee: feeRate * (bytesAccum + utxoBytes) } 22 | continue 23 | } 24 | 25 | bytesAccum += utxoBytes 26 | inAccum += utxoValue 27 | inputs.push(utxo) 28 | 29 | var fee = feeRate * bytesAccum 30 | 31 | // go again? 32 | if (inAccum < outAccum + fee) continue 33 | 34 | return utils.finalize(inputs, outputs, feeRate) 35 | } 36 | 37 | return { fee: feeRate * bytesAccum } 38 | } 39 | -------------------------------------------------------------------------------- /blackjack.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | 3 | // only add inputs if they don't bust the target value (aka, exact match) 4 | // worst-case: O(n) 5 | module.exports = function blackjack (utxos, outputs, feeRate) { 6 | if (!isFinite(utils.uintOrNaN(feeRate))) return {} 7 | 8 | var bytesAccum = utils.transactionBytes([], outputs) 9 | 10 | var inAccum = 0 11 | var inputs = [] 12 | var outAccum = utils.sumOrNaN(outputs) 13 | var threshold = utils.dustThreshold({}, feeRate) 14 | 15 | for (var i = 0; i < utxos.length; ++i) { 16 | var input = utxos[i] 17 | var inputBytes = utils.inputBytes(input) 18 | var fee = feeRate * (bytesAccum + inputBytes) 19 | var inputValue = utils.uintOrNaN(input.value) 20 | 21 | // would it waste value? 22 | if ((inAccum + inputValue) > (outAccum + fee + threshold)) continue 23 | 24 | bytesAccum += inputBytes 25 | inAccum += inputValue 26 | inputs.push(input) 27 | 28 | // go again? 29 | if (inAccum < outAccum + fee) continue 30 | 31 | return utils.finalize(inputs, outputs, feeRate) 32 | } 33 | 34 | return { fee: feeRate * bytesAccum } 35 | } 36 | -------------------------------------------------------------------------------- /break.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | 3 | // break utxos into the maximum number of 'output' possible 4 | module.exports = function broken (utxos, output, feeRate) { 5 | if (!isFinite(utils.uintOrNaN(feeRate))) return {} 6 | 7 | var bytesAccum = utils.transactionBytes(utxos, []) 8 | var value = utils.uintOrNaN(output.value) 9 | var inAccum = utils.sumOrNaN(utxos) 10 | if (!isFinite(value) || 11 | !isFinite(inAccum)) return { fee: feeRate * bytesAccum } 12 | 13 | var outputBytes = utils.outputBytes(output) 14 | var outAccum = 0 15 | var outputs = [] 16 | 17 | while (true) { 18 | var fee = feeRate * (bytesAccum + outputBytes) 19 | 20 | // did we bust? 21 | if (inAccum < (outAccum + fee + value)) { 22 | // premature? 23 | if (outAccum === 0) return { fee: fee } 24 | 25 | break 26 | } 27 | 28 | bytesAccum += outputBytes 29 | outAccum += value 30 | outputs.push(output) 31 | } 32 | 33 | return utils.finalize(utxos, outputs, feeRate) 34 | } 35 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export interface UTXO { 2 | txid: string | Buffer, 3 | vout: number, 4 | value: number, 5 | nonWitnessUtxo? : Buffer, 6 | witnessUtxo? : { 7 | script: Buffer, 8 | value: number 9 | } 10 | } 11 | export interface Target { 12 | address: string, 13 | value?: number 14 | } 15 | export interface SelectedUTXO { 16 | inputs?: UTXO[], 17 | outputs?: Target[], 18 | fee: number 19 | } 20 | export default function coinSelect(utxos: UTXO[], outputs: Target[], feeRate: number): SelectedUTXO; 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var accumulative = require('./accumulative') 2 | var blackjack = require('./blackjack') 3 | var utils = require('./utils') 4 | 5 | // order by descending value, minus the inputs approximate fee 6 | function utxoScore (x, feeRate) { 7 | return x.value - (feeRate * utils.inputBytes(x)) 8 | } 9 | 10 | module.exports = function coinSelect (utxos, outputs, feeRate) { 11 | utxos = utxos.concat().sort(function (a, b) { 12 | return utxoScore(b, feeRate) - utxoScore(a, feeRate) 13 | }) 14 | 15 | // attempt to use the blackjack strategy first (no change output) 16 | var base = blackjack(utxos, outputs, feeRate) 17 | if (base.inputs) return base 18 | 19 | // else, try the accumulative strategy 20 | return accumulative(utxos, outputs, feeRate) 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coinselect", 3 | "version": "3.1.13", 4 | "description": "A transaction input selection module for bitcoin.", 5 | "keywords": [ 6 | "coinselect", 7 | "coin", 8 | "unspents", 9 | "wallet", 10 | "BIP32", 11 | "management", 12 | "utxo", 13 | "transaction", 14 | "fee", 15 | "optimization", 16 | "optimizing", 17 | "bitcoin" 18 | ], 19 | "homepage": "https://github.com/bitcoinjs/coinselect", 20 | "bugs": { 21 | "url": "https://github.com/bitcoinjs/coinselect/issues" 22 | }, 23 | "license": "MIT", 24 | "author": "Daniel Cousens", 25 | "files": [ 26 | "accumulative.js", 27 | "blackjack.js", 28 | "break.js", 29 | "index.js", 30 | "split.js", 31 | "utils.js" 32 | ], 33 | "main": "index.js", 34 | "types": "index.d.ts", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/bitcoinjs/coinselect.git" 38 | }, 39 | "scripts": { 40 | "coverage": "nyc --check-coverage --branches 100 --functions 100 tape test/*.js", 41 | "standard": "standard", 42 | "test": "npm run standard && npm run unit", 43 | "unit": "tape test/*.js" 44 | }, 45 | "devDependencies": { 46 | "nyc": "^15.0.0", 47 | "standard": "^14.3.4", 48 | "tape": "^4.5.1" 49 | }, 50 | "dependencies": {} 51 | } 52 | -------------------------------------------------------------------------------- /split.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | 3 | // split utxos between each output, ignores outputs with .value defined 4 | module.exports = function split (utxos, outputs, feeRate) { 5 | if (!isFinite(utils.uintOrNaN(feeRate))) return {} 6 | 7 | var bytesAccum = utils.transactionBytes(utxos, outputs) 8 | var fee = feeRate * bytesAccum 9 | if (outputs.length === 0) return { fee: fee } 10 | 11 | var inAccum = utils.sumOrNaN(utxos) 12 | var outAccum = utils.sumForgiving(outputs) 13 | var remaining = inAccum - outAccum - fee 14 | if (!isFinite(remaining) || remaining < 0) return { fee: fee } 15 | 16 | var unspecified = outputs.reduce(function (a, x) { 17 | return a + !isFinite(x.value) 18 | }, 0) 19 | 20 | if (remaining === 0 && unspecified === 0) return utils.finalize(utxos, outputs, feeRate) 21 | 22 | var splitOutputsCount = outputs.reduce(function (a, x) { 23 | if (x.value !== undefined) return a 24 | return a + 1 25 | }, 0) 26 | var splitValue = Math.floor(remaining / splitOutputsCount) 27 | 28 | // ensure every output is either user defined, or over the threshold 29 | if (!outputs.every(function (x) { 30 | return x.value !== undefined || (splitValue > utils.dustThreshold(x, feeRate)) 31 | })) return { fee: fee } 32 | 33 | // assign splitValue to outputs not user defined 34 | outputs = outputs.map(function (x) { 35 | if (x.value !== undefined) return x 36 | 37 | // not user defined, but still copy over any non-value fields 38 | var y = {} 39 | for (var k in x) y[k] = x[k] 40 | y.value = splitValue 41 | return y 42 | }) 43 | 44 | return utils.finalize(utxos, outputs, feeRate) 45 | } 46 | -------------------------------------------------------------------------------- /stats/index.js: -------------------------------------------------------------------------------- 1 | const Simulation = require('./simulation') 2 | const modules = require('./strategies') 3 | const min = 14226 // 0.1 USD 4 | const max = 142251558 // 1000 USD 5 | const feeRate = 56 * 100 6 | const results = [] 7 | 8 | // switch between two modes 9 | // true - failed payments are discarded 10 | // false - failed payments are queued until there is enough balance 11 | const discardFailed = false 12 | const walletType = 'p2pkh' // set to either p2pkh or p2sh 13 | 14 | // data from blockchaing from ~august 2015 - august 2017 15 | // see https://gist.github.com/runn1ng/8e2881e5bb44e01748e46b3d1c549038 16 | // these are 2-of-3 lengths, most common p2sh (66%), percs changed so they add to 100 17 | const scripthashScriptLengthData = { 18 | prob: 0.16, 19 | inLengthPercs: { 20 | 252: 25.29, 21 | 253: 49.77, 22 | 254: 24.94 23 | }, 24 | outLength: 23 25 | } 26 | 27 | // these are compressed key length (90% of p2pkh inputs) 28 | // percs changed so they add to 100 29 | const pubkeyhashScriptLengthData = { 30 | prob: 0.84, 31 | inLengthPercs: { 32 | 105: 0.4, 33 | 106: 50.7, 34 | 107: 48.81, 35 | 108: 0.09 36 | }, 37 | outLength: 25 38 | } 39 | 40 | const selectedData = walletType === 'p2pkh' ? pubkeyhashScriptLengthData : scripthashScriptLengthData 41 | const inLengthProbs = selectedData.inLengthPercs 42 | 43 | const outLengthProbs = {}; 44 | [scripthashScriptLengthData, pubkeyhashScriptLengthData].forEach(({ prob, outLength }) => { 45 | outLengthProbs[outLength] = prob 46 | }) 47 | 48 | // n samples 49 | for (var j = 0; j < 100; ++j) { 50 | if (j % 200 === 0) console.log('Iteration', j) 51 | 52 | const stages = [] 53 | 54 | for (var i = 1; i < 4; ++i) { 55 | const utxos = Simulation.generateTxos(20 / i, min, max, inLengthProbs) 56 | const txos = Simulation.generateTxos(80 / i, min, max / 3, outLengthProbs) 57 | stages.push({ utxos, txos }) 58 | } 59 | 60 | // for each strategy 61 | for (var name in modules) { 62 | const f = modules[name] 63 | const simulation = new Simulation(name, f, feeRate) 64 | 65 | stages.forEach((stage) => { 66 | // supplement our UTXOs 67 | stage.utxos.forEach(x => { 68 | simulation.addUTXO(x) 69 | 70 | // if discardFailed == true, this should do nothing 71 | simulation.run(discardFailed) 72 | }) 73 | 74 | // now, run stage.txos.length transactions 75 | stage.txos.forEach((txo) => { 76 | simulation.plan([txo]) 77 | simulation.run(discardFailed) 78 | }) 79 | }) 80 | 81 | simulation.finish() 82 | results.push(simulation) 83 | } 84 | } 85 | 86 | function merge (results) { 87 | const resultMap = {} 88 | 89 | results.forEach(({ stats }) => { 90 | const result = resultMap[stats.name] 91 | 92 | if (result) { 93 | result.inputs += stats.inputs 94 | result.outputs += stats.outputs 95 | result.transactions += stats.transactions 96 | result.plannedTransactions += stats.plannedTransactions 97 | result.fees += stats.fees 98 | result.bytes += stats.bytes 99 | result.utxos += stats.utxos 100 | result.average = { 101 | nInputs: result.inputs / result.transactions, 102 | nOutputs: result.outputs / result.transactions, 103 | fee: Math.round(result.fees / result.transactions), 104 | feeRate: Math.round(result.fees / result.bytes) 105 | } 106 | result.totalCost += stats.totalCost 107 | } else { 108 | resultMap[stats.name] = Object.assign({}, stats) 109 | } 110 | }) 111 | 112 | return Object.keys(resultMap).map(k => ({ stats: resultMap[k] })) 113 | } 114 | 115 | function pad (i) { 116 | if (typeof i === 'number') i = Math.round(i * 1000) / 1000 117 | return (' ' + i).slice(-10) 118 | } 119 | 120 | merge(results).sort((a, b) => { 121 | if (a.stats.transactions !== b.stats.transactions) return b.stats.transactions - a.stats.transactions 122 | return a.stats.totalCost - b.stats.totalCost 123 | 124 | // top 20 only 125 | }).slice(0, 20).forEach((x, i) => { 126 | const { stats } = x 127 | const DNF = 1 - stats.transactions / stats.plannedTransactions 128 | 129 | console.log( 130 | pad(i), 131 | pad(stats.name), 132 | '| transactions', pad('' + stats.transactions), 133 | '| fee', pad('' + stats.average.fee), 134 | '| feeRate', pad('' + stats.average.feeRate), 135 | '| nInputs', pad(stats.average.nInputs), 136 | '| nOutputs', pad(stats.average.nOutputs), 137 | '| DNF', (100 * DNF).toFixed(2) + '%', 138 | '| totalCost', pad('' + Math.round(stats.totalCost / 1000)), 139 | '| utxos', pad('' + stats.utxos) 140 | ) 141 | }) 142 | -------------------------------------------------------------------------------- /stats/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stats", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "fisher-yates": "^1.0.3", 13 | "weighted": "^0.3.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /stats/simulation.js: -------------------------------------------------------------------------------- 1 | var weighted = require('weighted') 2 | 3 | function rayleight (a, b) { 4 | return a + b * Math.sqrt(-Math.log(uniform(0, 1))) 5 | } 6 | 7 | function uniform (min, max) { 8 | return min + (max - min) * Math.random() 9 | } 10 | 11 | function randomAddress () { 12 | return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[(Math.random() * 26) >>> 0] 13 | } 14 | 15 | var utils = require('../utils') 16 | 17 | function Simulation (name, algorithm, feeRate) { 18 | this.algorithm = algorithm 19 | this.feeRate = feeRate 20 | this.utxoMap = {} 21 | this.stats = { 22 | name, 23 | plannedTransactions: 0, 24 | transactions: 0, 25 | inputs: 0, 26 | outputs: 0, 27 | fees: 0, 28 | bytes: 0 29 | } 30 | 31 | this.planned = [] 32 | 33 | // used for tracking UTXOs (w/o transaction ids) 34 | this.k = 0 35 | } 36 | 37 | Simulation.generateTxos = function (n, min, max, scriptSizes) { 38 | const txos = [] 39 | for (let i = 0; i < n; ++i) { 40 | const v = rayleight(min, max) >>> 0 41 | 42 | const s = parseInt(weighted.select(scriptSizes)) 43 | 44 | txos.push({ 45 | address: randomAddress(), 46 | value: v, 47 | script: { 48 | length: s 49 | } 50 | }) 51 | } 52 | return txos 53 | } 54 | 55 | Simulation.prototype.addUTXO = function (utxo) { 56 | const k = this.k + 1 57 | if (this.utxoMap[k] !== undefined) throw new Error('Bad UTXO') 58 | 59 | this.utxoMap[k] = Object.assign({}, utxo, { id: k }) 60 | this.k = k 61 | } 62 | 63 | Simulation.prototype.useUTXO = function (utxo) { 64 | if (utxo.id === undefined) throw new Error('Bad UTXO') 65 | if (this.utxoMap[utxo.id] === undefined) throw new Error('Unknown UTXO') 66 | delete this.utxoMap[utxo.id] 67 | } 68 | 69 | Simulation.prototype.getUTXOs = function () { 70 | const utxos = [] 71 | for (const k in this.utxoMap) utxos.push(this.utxoMap[k]) 72 | return utxos 73 | } 74 | 75 | Simulation.prototype.plan = function (outputs) { 76 | this.stats.plannedTransactions += 1 77 | this.planned.push(outputs) 78 | } 79 | 80 | Simulation.prototype.run = function (discardFailed) { 81 | while (this.planned.length > 0) { 82 | const outputs = this.planned[0] 83 | const utxos = this.getUTXOs() 84 | 85 | const { inputs, outputs: outputs2, fee } = this.algorithm(utxos, outputs, this.feeRate) 86 | 87 | if (!inputs) { 88 | if (discardFailed) { 89 | this.planned.shift() 90 | } 91 | return 92 | } 93 | 94 | this.planned.shift() 95 | 96 | this.useResult(inputs, outputs2, fee) 97 | } 98 | } 99 | 100 | Simulation.prototype.useResult = function (inputs, outputs, fee) { 101 | this.stats.transactions += 1 102 | this.stats.inputs += inputs.length 103 | this.stats.outputs += outputs.length 104 | this.stats.fees += fee 105 | this.stats.bytes += utils.transactionBytes(inputs, outputs, this.feeRate) 106 | this.stats.average = { 107 | nInputs: this.stats.inputs / this.stats.transactions, 108 | nOutputs: this.stats.outputs / this.stats.transactions, 109 | fee: Math.round(this.stats.fees / this.stats.transactions), 110 | feeRate: Math.round(this.stats.fees / this.stats.bytes) 111 | } 112 | 113 | inputs.forEach(x => this.useUTXO(x)) 114 | 115 | // selected outputs w/ no script are change outputs, add them to the UTXO 116 | outputs.filter(x => x.script === undefined).forEach((x) => { 117 | // assign it a random address 118 | x.address = randomAddress() 119 | this.addUTXO(x) 120 | }) 121 | } 122 | 123 | Simulation.prototype.finish = function () { 124 | const utxos = this.getUTXOs() 125 | this.stats.utxos = utxos.length 126 | const costToEmpty = utils.transactionBytes(utxos, []) * this.feeRate // output cost is negligible 127 | this.stats.totalCost = this.stats.fees + costToEmpty 128 | } 129 | 130 | module.exports = Simulation 131 | -------------------------------------------------------------------------------- /stats/strategies.js: -------------------------------------------------------------------------------- 1 | const accumulative = require('../accumulative') 2 | const blackjack = require('../blackjack') 3 | const shuffle = require('fisher-yates') 4 | const shuffleInplace = require('fisher-yates/inplace') 5 | const coinSelect = require('../') 6 | const utils = require('../utils') 7 | 8 | function blackmax (utxos, outputs, feeRate) { 9 | // order by ascending value 10 | utxos = utxos.concat().sort((a, b) => a.value - b.value) 11 | 12 | // attempt to use the blackjack strategy first (no change output) 13 | const base = blackjack(utxos, outputs, feeRate) 14 | if (base.inputs) return base 15 | 16 | // else, try the accumulative strategy 17 | return accumulative(utxos, outputs, feeRate) 18 | } 19 | 20 | function blackmin (utxos, outputs, feeRate) { 21 | // order by descending value 22 | utxos = utxos.concat().sort((a, b) => b.value - a.value) 23 | 24 | // attempt to use the blackjack strategy first (no change output) 25 | const base = blackjack(utxos, outputs, feeRate) 26 | if (base.inputs) return base 27 | 28 | // else, try the accumulative strategy 29 | return accumulative(utxos, outputs, feeRate) 30 | } 31 | 32 | function blackrand (utxos, outputs, feeRate) { 33 | utxos = shuffle(utxos) 34 | 35 | // attempt to use the blackjack strategy first (no change output) 36 | const base = blackjack(utxos, outputs, feeRate) 37 | if (base.inputs) return base 38 | 39 | // else, try the accumulative strategy 40 | return accumulative(utxos, outputs, feeRate) 41 | } 42 | 43 | function maximal (utxos, outputs, feeRate) { 44 | utxos = utxos.concat().sort((a, b) => a.value - b.value) 45 | 46 | return accumulative(utxos, outputs, feeRate) 47 | } 48 | 49 | function minimal (utxos, outputs, feeRate) { 50 | utxos = utxos.concat().sort((a, b) => b.value - a.value) 51 | 52 | return accumulative(utxos, outputs, feeRate) 53 | } 54 | 55 | function FIFO (utxos, outputs, feeRate) { 56 | utxos = utxos.concat().reverse() 57 | 58 | return accumulative(utxos, outputs, feeRate) 59 | } 60 | 61 | function proximal (utxos, outputs, feeRate) { 62 | const outAccum = outputs.reduce((a, x) => a + x.value, 0) 63 | 64 | utxos = utxos.concat().sort((a, b) => { 65 | const aa = a.value - outAccum 66 | const bb = b.value - outAccum 67 | 68 | return aa - bb 69 | }) 70 | 71 | return accumulative(utxos, outputs, feeRate) 72 | } 73 | 74 | // similar to bitcoind 75 | function random (utxos, outputs, feeRate) { 76 | utxos = shuffle(utxos) 77 | 78 | return accumulative(utxos, outputs, feeRate) 79 | } 80 | 81 | function bestof (utxos, outputs, feeRate) { 82 | let n = 100 83 | const utxosCopy = utxos.concat() 84 | let best = { fee: Infinity } 85 | 86 | while (n) { 87 | shuffleInplace(utxosCopy) 88 | 89 | const result = accumulative(utxos, outputs, feeRate) 90 | if (result.inputs && result.fee < best.fee) { 91 | best = result 92 | } 93 | 94 | --n 95 | } 96 | 97 | return best 98 | } 99 | 100 | function utxoScore (x, feeRate) { 101 | return x.value - (feeRate * utils.inputBytes(x)) 102 | } 103 | 104 | function privet (utxos, outputs, feeRate) { 105 | const txosMap = {} 106 | utxos.forEach((txo) => { 107 | if (!txosMap[txo.address]) { 108 | txosMap[txo.address] = [] 109 | } 110 | 111 | txosMap[txo.address].push(txo) 112 | }) 113 | 114 | // order & summate sets 115 | for (var address in txosMap) { 116 | txosMap[address] = txosMap[address].sort((a, b) => { 117 | return utxoScore(b, feeRate) - utxoScore(a, feeRate) 118 | }) 119 | txosMap[address].value = txosMap[address].reduce((a, x) => a + x.value, 0) 120 | } 121 | 122 | utxos = [].concat.apply([], Object.keys(txosMap).map(x => txosMap[x])) 123 | 124 | // only use accumulative strategy 125 | return accumulative(utxos, outputs, feeRate) 126 | } 127 | 128 | module.exports = { 129 | accumulative, 130 | bestof, 131 | blackjack, 132 | blackmax, 133 | blackmin, 134 | blackrand, 135 | coinSelect, 136 | FIFO, 137 | maximal, 138 | minimal, 139 | privet, 140 | proximal, 141 | random 142 | } 143 | -------------------------------------------------------------------------------- /test/_utils.js: -------------------------------------------------------------------------------- 1 | function expand (values, indices) { 2 | if (indices) { 3 | return values.map(function (x, i) { 4 | if (typeof x === 'number') return { i: i, value: x } 5 | 6 | var y = { i: i } 7 | for (var k in x) y[k] = x[k] 8 | return y 9 | }) 10 | } 11 | 12 | return values.map(function (x, i) { 13 | return typeof x === 'object' ? x : { value: x } 14 | }) 15 | } 16 | 17 | function testValues (t, actual, expected) { 18 | t.equal(typeof actual, typeof expected, 'types match') 19 | if (!expected) return 20 | 21 | t.equal(actual.length, expected.length, 'lengths match') 22 | 23 | actual.forEach(function (ai, i) { 24 | var ei = expected[i] 25 | 26 | if (ai.i !== undefined) { 27 | t.equal(ai.i, ei, 'indexes match') 28 | } else if (typeof ei === 'number') { 29 | t.equal(ai.value, ei, 'values match') 30 | } else { 31 | t.same(ai, ei, 'objects match') 32 | } 33 | }) 34 | } 35 | 36 | module.exports = { 37 | expand: expand, 38 | testValues: testValues 39 | } 40 | -------------------------------------------------------------------------------- /test/accumulative.js: -------------------------------------------------------------------------------- 1 | var coinAccum = require('../accumulative') 2 | var fixtures = require('./fixtures/accumulative') 3 | var tape = require('tape') 4 | var utils = require('./_utils') 5 | 6 | fixtures.forEach(function (f) { 7 | tape(f.description, function (t) { 8 | var inputs = utils.expand(f.inputs, true) 9 | var outputs = utils.expand(f.outputs) 10 | var actual = coinAccum(inputs, outputs, f.feeRate) 11 | 12 | t.same(actual, f.expected) 13 | if (actual.inputs) { 14 | var feedback = coinAccum(actual.inputs, actual.outputs, f.feeRate) 15 | t.same(feedback, f.expected) 16 | } 17 | 18 | t.end() 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/break.js: -------------------------------------------------------------------------------- 1 | var coinBreak = require('../break') 2 | var fixtures = require('./fixtures/break') 3 | var tape = require('tape') 4 | var utils = require('./_utils') 5 | 6 | fixtures.forEach(function (f) { 7 | tape(f.description, function (t) { 8 | var finputs = utils.expand(f.inputs) 9 | var foutputs = utils.expand([f.output]) 10 | var actual = coinBreak(finputs, foutputs[0], f.feeRate) 11 | 12 | t.same(actual, f.expected) 13 | t.end() 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/fixtures/accumulative.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "1 output, no change", 4 | "feeRate": 10, 5 | "inputs": [ 6 | 102001 7 | ], 8 | "outputs": [ 9 | 100000 10 | ], 11 | "expected": { 12 | "inputs": [ 13 | { 14 | "i": 0, 15 | "value": 102001 16 | } 17 | ], 18 | "outputs": [ 19 | { 20 | "value": 100000 21 | } 22 | ], 23 | "fee": 2001 24 | } 25 | }, 26 | { 27 | "description": "1 output, change expected", 28 | "feeRate": 5, 29 | "inputs": [ 30 | 106001 31 | ], 32 | "outputs": [ 33 | 100000 34 | ], 35 | "expected": { 36 | "inputs": [ 37 | { 38 | "i": 0, 39 | "value": 106001 40 | } 41 | ], 42 | "outputs": [ 43 | { 44 | "value": 100000 45 | }, 46 | { 47 | "value": 4871 48 | } 49 | ], 50 | "fee": 1130 51 | } 52 | }, 53 | { 54 | "description": "1 output, change expected, value > 2^32", 55 | "feeRate": 5, 56 | "inputs": [ 57 | 5000000000 58 | ], 59 | "outputs": [ 60 | 1 61 | ], 62 | "expected": { 63 | "inputs": [ 64 | { 65 | "i": 0, 66 | "value": 5000000000 67 | } 68 | ], 69 | "outputs": [ 70 | { 71 | "value": 1 72 | }, 73 | { 74 | "value": 4999998869 75 | } 76 | ], 77 | "fee": 1130 78 | } 79 | }, 80 | { 81 | "description": "1 output, sub-optimal inputs (if re-ordered), direct possible", 82 | "feeRate": 10, 83 | "inputs": [ 84 | 10000, 85 | 40000, 86 | 40000 87 | ], 88 | "outputs": [ 89 | 7700 90 | ], 91 | "expected": { 92 | "inputs": [ 93 | { 94 | "i": 0, 95 | "value": 10000 96 | } 97 | ], 98 | "outputs": [ 99 | { 100 | "value": 7700 101 | } 102 | ], 103 | "fee": 2300 104 | } 105 | }, 106 | { 107 | "description": "1 output, sub-optimal inputs (if re-ordered), direct possible, but slightly higher fee", 108 | "feeRate": 10, 109 | "inputs": [ 110 | 10000, 111 | 40000, 112 | 40000 113 | ], 114 | "outputs": [ 115 | 6800 116 | ], 117 | "expected": { 118 | "inputs": [ 119 | { 120 | "i": 0, 121 | "value": 10000 122 | } 123 | ], 124 | "outputs": [ 125 | { 126 | "value": 6800 127 | } 128 | ], 129 | "fee": 3200 130 | } 131 | }, 132 | { 133 | "description": "1 output, sub-optimal inputs (if re-ordered, no direct possible), change expected", 134 | "feeRate": 5, 135 | "inputs": [ 136 | 10000, 137 | 40000, 138 | 40000 139 | ], 140 | "outputs": [ 141 | 4700 142 | ], 143 | "expected": { 144 | "inputs": [ 145 | { 146 | "i": 0, 147 | "value": 10000 148 | } 149 | ], 150 | "outputs": [ 151 | { 152 | "value": 4700 153 | }, 154 | { 155 | "value": 4170 156 | } 157 | ], 158 | "fee": 1130 159 | } 160 | }, 161 | { 162 | "description": "1 output, passes, skipped detrimental input", 163 | "feeRate": 5, 164 | "inputs": [ 165 | { 166 | "script": { 167 | "length": 1000 168 | }, 169 | "value": 3000 170 | }, 171 | { 172 | "value": 3000 173 | }, 174 | { 175 | "value": 3000 176 | } 177 | ], 178 | "outputs": [ 179 | 4000 180 | ], 181 | "expected": { 182 | "fee": 2000, 183 | "inputs": [ 184 | { 185 | "i": 1, 186 | "value": 3000 187 | }, 188 | { 189 | "i": 2, 190 | "value": 3000 191 | } 192 | ], 193 | "outputs": [ 194 | { 195 | "value": 4000 196 | } 197 | ] 198 | } 199 | }, 200 | { 201 | "description": "1 output, fails, skips (and finishes on) detrimental input", 202 | "feeRate": 55, 203 | "inputs": [ 204 | { 205 | "value": 44000 206 | }, 207 | { 208 | "value": 800 209 | } 210 | ], 211 | "outputs": [ 212 | 38000 213 | ], 214 | "expected": { 215 | "fee": 18700 216 | } 217 | }, 218 | { 219 | "description": "1 output, passes, poor ordering causing high fee", 220 | "feeRate": 5, 221 | "inputs": [ 222 | { 223 | "script": { 224 | "length": 500 225 | }, 226 | "value": 3000 227 | }, 228 | { 229 | "value": 3000 230 | }, 231 | { 232 | "value": 3000 233 | } 234 | ], 235 | "outputs": [ 236 | 4000 237 | ], 238 | "expected": { 239 | "inputs": [ 240 | { 241 | "i": 0, 242 | "script": { 243 | "length": 500 244 | }, 245 | "value": 3000 246 | }, 247 | { 248 | "i": 1, 249 | "value": 3000 250 | }, 251 | { 252 | "i": 2, 253 | "value": 3000 254 | } 255 | ], 256 | "outputs": [ 257 | { 258 | "value": 4000 259 | } 260 | ], 261 | "fee": 5000 262 | } 263 | }, 264 | { 265 | "description": "1 output, passes, improved ordering causing low fee, no waste", 266 | "feeRate": 5, 267 | "inputs": [ 268 | { 269 | "value": 3000 270 | }, 271 | { 272 | "value": 3000 273 | }, 274 | { 275 | "script": { 276 | "length": 400 277 | }, 278 | "value": 3000 279 | } 280 | ], 281 | "outputs": [ 282 | 4000 283 | ], 284 | "expected": { 285 | "inputs": [ 286 | { 287 | "i": 0, 288 | "value": 3000 289 | }, 290 | { 291 | "i": 1, 292 | "value": 3000 293 | } 294 | ], 295 | "outputs": [ 296 | { 297 | "value": 4000 298 | } 299 | ], 300 | "fee": 2000 301 | } 302 | }, 303 | { 304 | "description": "1 output, optimal inputs, no change", 305 | "feeRate": 10, 306 | "inputs": [ 307 | 10000 308 | ], 309 | "outputs": [ 310 | 7700 311 | ], 312 | "expected": { 313 | "inputs": [ 314 | { 315 | "i": 0, 316 | "value": 10000 317 | } 318 | ], 319 | "outputs": [ 320 | { 321 | "value": 7700 322 | } 323 | ], 324 | "fee": 2300 325 | } 326 | }, 327 | { 328 | "description": "1 output, no fee, change expected", 329 | "feeRate": 0, 330 | "inputs": [ 331 | 5000, 332 | 5000, 333 | 5000, 334 | 5000, 335 | 5000, 336 | 5000 337 | ], 338 | "outputs": [ 339 | 28000 340 | ], 341 | "expected": { 342 | "inputs": [ 343 | { 344 | "i": 0, 345 | "value": 5000 346 | }, 347 | { 348 | "i": 1, 349 | "value": 5000 350 | }, 351 | { 352 | "i": 2, 353 | "value": 5000 354 | }, 355 | { 356 | "i": 3, 357 | "value": 5000 358 | }, 359 | { 360 | "i": 4, 361 | "value": 5000 362 | }, 363 | { 364 | "i": 5, 365 | "value": 5000 366 | } 367 | ], 368 | "outputs": [ 369 | { 370 | "value": 28000 371 | }, 372 | { 373 | "value": 2000 374 | } 375 | ], 376 | "fee": 0 377 | } 378 | }, 379 | { 380 | "description": "1 output, script provided, no change", 381 | "feeRate": 10, 382 | "inputs": [ 383 | 100000 384 | ], 385 | "outputs": [ 386 | { 387 | "script": { 388 | "length": 200 389 | }, 390 | "value": 95000 391 | } 392 | ], 393 | "expected": { 394 | "inputs": [ 395 | { 396 | "i": 0, 397 | "value": 100000 398 | } 399 | ], 400 | "outputs": [ 401 | { 402 | "script": { 403 | "length": 200 404 | }, 405 | "value": 95000 406 | } 407 | ], 408 | "fee": 5000 409 | } 410 | }, 411 | { 412 | "description": "1 output, script provided, change expected", 413 | "feeRate": 10, 414 | "inputs": [ 415 | 200000 416 | ], 417 | "outputs": [ 418 | { 419 | "script": { 420 | "length": 200 421 | }, 422 | "value": 95000 423 | } 424 | ], 425 | "expected": { 426 | "inputs": [ 427 | { 428 | "i": 0, 429 | "value": 200000 430 | } 431 | ], 432 | "outputs": [ 433 | { 434 | "script": { 435 | "length": 200 436 | }, 437 | "value": 95000 438 | }, 439 | { 440 | "value": 100990 441 | } 442 | ], 443 | "fee": 4010 444 | } 445 | }, 446 | { 447 | "description": "1 output, 2 inputs (related), no change", 448 | "feeRate": 10, 449 | "inputs": [ 450 | { 451 | "address": "a", 452 | "value": 100000 453 | }, 454 | { 455 | "address": "a", 456 | "value": 2000 457 | } 458 | ], 459 | "outputs": [ 460 | 98000 461 | ], 462 | "expected": { 463 | "inputs": [ 464 | { 465 | "i": 0, 466 | "address": "a", 467 | "value": 100000 468 | } 469 | ], 470 | "outputs": [ 471 | { 472 | "value": 98000 473 | } 474 | ], 475 | "fee": 2000 476 | } 477 | }, 478 | { 479 | "description": "many outputs, no change", 480 | "feeRate": 10, 481 | "inputs": [ 482 | 30000, 483 | 12220, 484 | 10001 485 | ], 486 | "outputs": [ 487 | 35000, 488 | 5000, 489 | 5000, 490 | 1000 491 | ], 492 | "expected": { 493 | "inputs": [ 494 | { 495 | "i": 0, 496 | "value": 30000 497 | }, 498 | { 499 | "i": 1, 500 | "value": 12220 501 | }, 502 | { 503 | "i": 2, 504 | "value": 10001 505 | } 506 | ], 507 | "outputs": [ 508 | { 509 | "value": 35000 510 | }, 511 | { 512 | "value": 5000 513 | }, 514 | { 515 | "value": 5000 516 | }, 517 | { 518 | "value": 1000 519 | } 520 | ], 521 | "fee": 6221 522 | } 523 | }, 524 | { 525 | "description": "many outputs, change expected", 526 | "feeRate": 10, 527 | "inputs": [ 528 | 30000, 529 | 14220, 530 | 10001 531 | ], 532 | "outputs": [ 533 | 35000, 534 | 5000, 535 | 5000, 536 | 1000 537 | ], 538 | "expected": { 539 | "inputs": [ 540 | { 541 | "i": 0, 542 | "value": 30000 543 | }, 544 | { 545 | "i": 1, 546 | "value": 14220 547 | }, 548 | { 549 | "i": 2, 550 | "value": 10001 551 | } 552 | ], 553 | "outputs": [ 554 | { 555 | "value": 35000 556 | }, 557 | { 558 | "value": 5000 559 | }, 560 | { 561 | "value": 5000 562 | }, 563 | { 564 | "value": 1000 565 | }, 566 | { 567 | "value": 1981 568 | } 569 | ], 570 | "fee": 6240 571 | } 572 | }, 573 | { 574 | "description": "many outputs, no fee, change expected", 575 | "feeRate": 0, 576 | "inputs": [ 577 | 5000, 578 | 5000, 579 | 5000, 580 | 5000, 581 | 5000, 582 | 5000 583 | ], 584 | "outputs": [ 585 | 28000, 586 | 1000 587 | ], 588 | "expected": { 589 | "inputs": [ 590 | { 591 | "i": 0, 592 | "value": 5000 593 | }, 594 | { 595 | "i": 1, 596 | "value": 5000 597 | }, 598 | { 599 | "i": 2, 600 | "value": 5000 601 | }, 602 | { 603 | "i": 3, 604 | "value": 5000 605 | }, 606 | { 607 | "i": 4, 608 | "value": 5000 609 | }, 610 | { 611 | "i": 5, 612 | "value": 5000 613 | } 614 | ], 615 | "outputs": [ 616 | { 617 | "value": 28000 618 | }, 619 | { 620 | "value": 1000 621 | }, 622 | { 623 | "value": 1000 624 | } 625 | ], 626 | "fee": 0 627 | } 628 | }, 629 | { 630 | "description": "no outputs, no change", 631 | "feeRate": 10, 632 | "inputs": [ 633 | 1900 634 | ], 635 | "outputs": [], 636 | "expected": { 637 | "inputs": [ 638 | { 639 | "i": 0, 640 | "value": 1900 641 | } 642 | ], 643 | "outputs": [], 644 | "fee": 1900 645 | } 646 | }, 647 | { 648 | "description": "no outputs, change expected", 649 | "feeRate": 10, 650 | "inputs": [ 651 | 20000 652 | ], 653 | "outputs": [], 654 | "expected": { 655 | "inputs": [ 656 | { 657 | "i": 0, 658 | "value": 20000 659 | } 660 | ], 661 | "outputs": [ 662 | { 663 | "value": 18080 664 | } 665 | ], 666 | "fee": 1920 667 | } 668 | }, 669 | { 670 | "description": "inputs used in order of DESCENDING", 671 | "feeRate": 10, 672 | "inputs": [ 673 | 20000, 674 | { 675 | "script": { 676 | "length": 300 677 | }, 678 | "value": 10000 679 | }, 680 | 10000 681 | ], 682 | "outputs": [ 683 | 25000 684 | ], 685 | "expected": { 686 | "fee": 7150, 687 | "inputs": [ 688 | { 689 | "i": 0, 690 | "value": 20000 691 | }, 692 | { 693 | "i": 1, 694 | "script": { 695 | "length": 300 696 | }, 697 | "value": 10000 698 | }, 699 | { 700 | "i": 2, 701 | "value": 10000 702 | } 703 | ], 704 | "outputs": [ 705 | { 706 | "value": 25000 707 | }, 708 | { 709 | "value": 7850 710 | } 711 | ] 712 | } 713 | }, 714 | { 715 | "description": "not enough funds, empty result", 716 | "feeRate": 10, 717 | "inputs": [ 718 | 20000 719 | ], 720 | "outputs": [ 721 | 40000 722 | ], 723 | "expected": { 724 | "fee": 1920 725 | } 726 | }, 727 | { 728 | "description": "not enough funds (w/ fee), empty result", 729 | "feeRate": 10, 730 | "inputs": [ 731 | 40000 732 | ], 733 | "outputs": [ 734 | 40000 735 | ], 736 | "expected": { 737 | "fee": 1920 738 | } 739 | }, 740 | { 741 | "description": "not enough funds (no inputs), empty result", 742 | "feeRate": 10, 743 | "inputs": [], 744 | "outputs": [], 745 | "expected": { 746 | "fee": 100 747 | } 748 | }, 749 | { 750 | "description": "not enough funds (no inputs), empty result (>1KiB)", 751 | "feeRate": 10, 752 | "inputs": [], 753 | "outputs": [ 754 | 1, 755 | 1, 756 | 1, 757 | 1, 758 | 1, 759 | 1, 760 | 1, 761 | 1, 762 | 1, 763 | 1, 764 | 1, 765 | 1, 766 | 1, 767 | 1, 768 | 1, 769 | 1, 770 | 1, 771 | 1, 772 | 1, 773 | 1, 774 | 1, 775 | 1, 776 | 1, 777 | 1, 778 | 1, 779 | 1, 780 | 1, 781 | 1, 782 | 1 783 | ], 784 | "expected": { 785 | "fee": 9960 786 | } 787 | }, 788 | { 789 | "description": "2 outputs, some with missing value (NaN)", 790 | "feeRate": 10, 791 | "inputs": [ 792 | 20000 793 | ], 794 | "outputs": [ 795 | 1000, 796 | {} 797 | ], 798 | "expected": { 799 | "fee": 2260 800 | } 801 | }, 802 | { 803 | "description": "input with float values (NaN)", 804 | "feeRate": 10, 805 | "inputs": [ 806 | 20000.5 807 | ], 808 | "outputs": [ 809 | 10000, 810 | 1200 811 | ], 812 | "expected": { 813 | "fee": 2260 814 | } 815 | }, 816 | { 817 | "description": "2 outputs, with float values (NaN)", 818 | "feeRate": 10, 819 | "inputs": [ 820 | 20000 821 | ], 822 | "outputs": [ 823 | 10000.25, 824 | 1200.5 825 | ], 826 | "expected": { 827 | "fee": 2260 828 | } 829 | }, 830 | { 831 | "description": "2 outputs, string values (NaN)", 832 | "feeRate": 10, 833 | "inputs": [ 834 | 20000 835 | ], 836 | "outputs": [ 837 | { 838 | "value": "100" 839 | }, 840 | { 841 | "value": "204" 842 | } 843 | ], 844 | "expected": { 845 | "fee": 2260 846 | } 847 | }, 848 | { 849 | "description": "inputs and outputs, bad feeRate (NaN)", 850 | "feeRate": "1", 851 | "inputs": [ 852 | 20000 853 | ], 854 | "outputs": [ 855 | 10000 856 | ], 857 | "expected": {} 858 | }, 859 | { 860 | "description": "inputs and outputs, bad feeRate (NaN)", 861 | "feeRate": 1.5, 862 | "inputs": [ 863 | 20000 864 | ], 865 | "outputs": [ 866 | 10000 867 | ], 868 | "expected": {} 869 | } 870 | ] 871 | -------------------------------------------------------------------------------- /test/fixtures/break.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "1:1, no remainder", 4 | "feeRate": 10, 5 | "inputs": [ 6 | 11920 7 | ], 8 | "output": 10000, 9 | "expected": { 10 | "inputs": [ 11 | { 12 | "value": 11920 13 | } 14 | ], 15 | "outputs": [ 16 | { 17 | "value": 10000 18 | } 19 | ], 20 | "fee": 1920 21 | } 22 | }, 23 | { 24 | "description": "1:1", 25 | "feeRate": 10, 26 | "inputs": [ 27 | 12000 28 | ], 29 | "output": { 30 | "address": "woop", 31 | "value": 10000 32 | }, 33 | "expected": { 34 | "fee": 2000, 35 | "inputs": [ 36 | { 37 | "value": 12000 38 | } 39 | ], 40 | "outputs": [ 41 | { 42 | "address": "woop", 43 | "value": 10000 44 | } 45 | ] 46 | } 47 | }, 48 | { 49 | "description": "1:1, w/ change", 50 | "feeRate": 10, 51 | "inputs": [ 52 | 12000 53 | ], 54 | "output": 8000, 55 | "expected": { 56 | "inputs": [ 57 | { 58 | "value": 12000 59 | } 60 | ], 61 | "outputs": [ 62 | { 63 | "value": 8000 64 | }, 65 | { 66 | "value": 1740 67 | } 68 | ], 69 | "fee": 2260 70 | } 71 | }, 72 | { 73 | "description": "1:2, strange output type", 74 | "feeRate": 10, 75 | "inputs": [ 76 | 27000 77 | ], 78 | "output": { 79 | "script": { 80 | "length": 220 81 | }, 82 | "value": 10000 83 | }, 84 | "expected": { 85 | "inputs": [ 86 | { 87 | "value": 27000 88 | } 89 | ], 90 | "outputs": [ 91 | { 92 | "script": { 93 | "length": 220 94 | }, 95 | "value": 10000 96 | }, 97 | { 98 | "script": { 99 | "length": 220 100 | }, 101 | "value": 10000 102 | } 103 | ], 104 | "fee": 7000 105 | } 106 | }, 107 | { 108 | "description": "1:4", 109 | "feeRate": 10, 110 | "inputs": [ 111 | 12000 112 | ], 113 | "output": 2000, 114 | "expected": { 115 | "inputs": [ 116 | { 117 | "value": 12000 118 | } 119 | ], 120 | "outputs": [ 121 | { 122 | "value": 2000 123 | }, 124 | { 125 | "value": 2000 126 | }, 127 | { 128 | "value": 2000 129 | }, 130 | { 131 | "value": 2000 132 | } 133 | ], 134 | "fee": 4000 135 | } 136 | }, 137 | { 138 | "description": "2:5", 139 | "feeRate": 10, 140 | "inputs": [ 141 | 3000, 142 | 12000 143 | ], 144 | "output": 2000, 145 | "expected": { 146 | "inputs": [ 147 | { 148 | "value": 3000 149 | }, 150 | { 151 | "value": 12000 152 | } 153 | ], 154 | "outputs": [ 155 | { 156 | "value": 2000 157 | }, 158 | { 159 | "value": 2000 160 | }, 161 | { 162 | "value": 2000 163 | }, 164 | { 165 | "value": 2000 166 | }, 167 | { 168 | "value": 2000 169 | } 170 | ], 171 | "fee": 5000 172 | } 173 | }, 174 | { 175 | "description": "2:5, no fee", 176 | "feeRate": 0, 177 | "inputs": [ 178 | 5000, 179 | 10000 180 | ], 181 | "output": 3000, 182 | "expected": { 183 | "inputs": [ 184 | { 185 | "value": 5000 186 | }, 187 | { 188 | "value": 10000 189 | } 190 | ], 191 | "outputs": [ 192 | { 193 | "value": 3000 194 | }, 195 | { 196 | "value": 3000 197 | }, 198 | { 199 | "value": 3000 200 | }, 201 | { 202 | "value": 3000 203 | }, 204 | { 205 | "value": 3000 206 | } 207 | ], 208 | "fee": 0 209 | } 210 | }, 211 | { 212 | "description": "2:2 (+1), w/ change", 213 | "feeRate": 7, 214 | "inputs": [ 215 | 16000 216 | ], 217 | "output": 6000, 218 | "expected": { 219 | "inputs": [ 220 | { 221 | "value": 16000 222 | } 223 | ], 224 | "outputs": [ 225 | { 226 | "value": 6000 227 | }, 228 | { 229 | "value": 6000 230 | }, 231 | { 232 | "value": 2180 233 | } 234 | ], 235 | "fee": 1820 236 | } 237 | }, 238 | { 239 | "description": "2:3 (+1), no fee, w/ change", 240 | "feeRate": 0, 241 | "inputs": [ 242 | 5000, 243 | 10000 244 | ], 245 | "output": 4000, 246 | "expected": { 247 | "inputs": [ 248 | { 249 | "value": 5000 250 | }, 251 | { 252 | "value": 10000 253 | } 254 | ], 255 | "outputs": [ 256 | { 257 | "value": 4000 258 | }, 259 | { 260 | "value": 4000 261 | }, 262 | { 263 | "value": 4000 264 | }, 265 | { 266 | "value": 3000 267 | } 268 | ], 269 | "fee": 0 270 | } 271 | }, 272 | { 273 | "description": "not enough funds", 274 | "feeRate": 10, 275 | "inputs": [ 276 | 41000, 277 | 1000 278 | ], 279 | "output": 40000, 280 | "expected": { 281 | "fee": 3400 282 | } 283 | }, 284 | { 285 | "description": "no inputs", 286 | "feeRate": 10, 287 | "inputs": [], 288 | "output": 2000, 289 | "expected": { 290 | "fee": 440 291 | } 292 | }, 293 | { 294 | "description": "invalid output (NaN)", 295 | "feeRate": 10, 296 | "inputs": [], 297 | "output": {}, 298 | "expected": { 299 | "fee": 100 300 | } 301 | }, 302 | { 303 | "description": "input with float values (NaN)", 304 | "feeRate": 10, 305 | "inputs": [ 306 | 10000.5 307 | ], 308 | "output": 5000, 309 | "expected": { 310 | "fee": 1580 311 | } 312 | }, 313 | { 314 | "description": "inputs and outputs, bad feeRate (NaN)", 315 | "feeRate": "1", 316 | "inputs": [ 317 | 20000 318 | ], 319 | "output": 10000, 320 | "expected": {} 321 | }, 322 | { 323 | "description": "inputs and outputs, bad feeRate (NaN)", 324 | "feeRate": 1.5, 325 | "inputs": [ 326 | 20000 327 | ], 328 | "output": 10000, 329 | "expected": {} 330 | } 331 | ] 332 | -------------------------------------------------------------------------------- /test/fixtures/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "1 output, no change", 4 | "feeRate": 10, 5 | "inputs": [ 6 | 102001 7 | ], 8 | "outputs": [ 9 | 100000 10 | ], 11 | "expected": { 12 | "inputs": [ 13 | { 14 | "i": 0, 15 | "value": 102001 16 | } 17 | ], 18 | "outputs": [ 19 | { 20 | "value": 100000 21 | } 22 | ], 23 | "fee": 2001 24 | } 25 | }, 26 | { 27 | "description": "1 output, change expected", 28 | "feeRate": 5, 29 | "inputs": [ 30 | 106001 31 | ], 32 | "outputs": [ 33 | 100000 34 | ], 35 | "expected": { 36 | "inputs": [ 37 | { 38 | "i": 0, 39 | "value": 106001 40 | } 41 | ], 42 | "outputs": [ 43 | { 44 | "value": 100000 45 | }, 46 | { 47 | "value": 4871 48 | } 49 | ], 50 | "fee": 1130 51 | } 52 | }, 53 | { 54 | "description": "1 output, sub-optimal inputs (if re-ordered), direct possible", 55 | "feeRate": 10, 56 | "inputs": [ 57 | 10000, 58 | 40000, 59 | 40000 60 | ], 61 | "outputs": [ 62 | 7700 63 | ], 64 | "expected": { 65 | "inputs": [ 66 | { 67 | "i": 0, 68 | "value": 10000 69 | } 70 | ], 71 | "outputs": [ 72 | { 73 | "value": 7700 74 | } 75 | ], 76 | "fee": 2300 77 | } 78 | }, 79 | { 80 | "description": "1 output, sub-optimal inputs (if re-ordered), direct possible, but slightly higher fee", 81 | "feeRate": 10, 82 | "inputs": [ 83 | 10000, 84 | 40000, 85 | 40000 86 | ], 87 | "outputs": [ 88 | 6800 89 | ], 90 | "expected": { 91 | "inputs": [ 92 | { 93 | "i": 0, 94 | "value": 10000 95 | } 96 | ], 97 | "outputs": [ 98 | { 99 | "value": 6800 100 | } 101 | ], 102 | "fee": 3200 103 | } 104 | }, 105 | { 106 | "description": "1 output, sub-optimal inputs (if re-ordered, no direct possible), change expected", 107 | "feeRate": 5, 108 | "inputs": [ 109 | 10000, 110 | 40000, 111 | 40000 112 | ], 113 | "outputs": [ 114 | 4700 115 | ], 116 | "expected": { 117 | "inputs": [ 118 | { 119 | "i": 1, 120 | "value": 40000 121 | } 122 | ], 123 | "outputs": [ 124 | { 125 | "value": 4700 126 | }, 127 | { 128 | "value": 34170 129 | } 130 | ], 131 | "fee": 1130 132 | } 133 | }, 134 | { 135 | "description": "1 output, passes, skipped detrimental input", 136 | "feeRate": 5, 137 | "inputs": [ 138 | { 139 | "script": { 140 | "length": 1000 141 | }, 142 | "value": 3000 143 | }, 144 | { 145 | "value": 3000 146 | }, 147 | { 148 | "value": 3000 149 | } 150 | ], 151 | "outputs": [ 152 | 4000 153 | ], 154 | "expected": { 155 | "fee": 2000, 156 | "inputs": [ 157 | { 158 | "i": 1, 159 | "value": 3000 160 | }, 161 | { 162 | "i": 2, 163 | "value": 3000 164 | } 165 | ], 166 | "outputs": [ 167 | { 168 | "value": 4000 169 | } 170 | ] 171 | } 172 | }, 173 | { 174 | "description": "1 output, passes, poor ordering but still good", 175 | "feeRate": 5, 176 | "inputs": [ 177 | { 178 | "script": { 179 | "length": 500 180 | }, 181 | "value": 3000 182 | }, 183 | { 184 | "value": 3000 185 | }, 186 | { 187 | "value": 3000 188 | } 189 | ], 190 | "outputs": [ 191 | 4000 192 | ], 193 | "expected": { 194 | "inputs": [ 195 | { 196 | "i": 1, 197 | "value": 3000 198 | }, 199 | { 200 | "i": 2, 201 | "value": 3000 202 | } 203 | ], 204 | "outputs": [ 205 | { 206 | "value": 4000 207 | } 208 | ], 209 | "fee": 2000 210 | } 211 | }, 212 | { 213 | "description": "1 output, passes, improved ordering causing low fee, no waste", 214 | "feeRate": 5, 215 | "inputs": [ 216 | { 217 | "value": 3000 218 | }, 219 | { 220 | "value": 3000 221 | }, 222 | { 223 | "script": { 224 | "length": 400 225 | }, 226 | "value": 3000 227 | } 228 | ], 229 | "outputs": [ 230 | 4000 231 | ], 232 | "expected": { 233 | "inputs": [ 234 | { 235 | "i": 0, 236 | "value": 3000 237 | }, 238 | { 239 | "i": 1, 240 | "value": 3000 241 | } 242 | ], 243 | "outputs": [ 244 | { 245 | "value": 4000 246 | } 247 | ], 248 | "fee": 2000 249 | } 250 | }, 251 | { 252 | "description": "1 output, optimal inputs, no change", 253 | "feeRate": 10, 254 | "inputs": [ 255 | 10000 256 | ], 257 | "outputs": [ 258 | 7700 259 | ], 260 | "expected": { 261 | "inputs": [ 262 | { 263 | "i": 0, 264 | "value": 10000 265 | } 266 | ], 267 | "outputs": [ 268 | { 269 | "value": 7700 270 | } 271 | ], 272 | "fee": 2300 273 | } 274 | }, 275 | { 276 | "description": "1 output, no fee, change expected", 277 | "feeRate": 0, 278 | "inputs": [ 279 | 5000, 280 | 5000, 281 | 5000, 282 | 5000, 283 | 5000, 284 | 5000 285 | ], 286 | "outputs": [ 287 | 28000 288 | ], 289 | "expected": { 290 | "inputs": [ 291 | { 292 | "i": 0, 293 | "value": 5000 294 | }, 295 | { 296 | "i": 1, 297 | "value": 5000 298 | }, 299 | { 300 | "i": 2, 301 | "value": 5000 302 | }, 303 | { 304 | "i": 3, 305 | "value": 5000 306 | }, 307 | { 308 | "i": 4, 309 | "value": 5000 310 | }, 311 | { 312 | "i": 5, 313 | "value": 5000 314 | } 315 | ], 316 | "outputs": [ 317 | { 318 | "value": 28000 319 | }, 320 | { 321 | "value": 2000 322 | } 323 | ], 324 | "fee": 0 325 | } 326 | }, 327 | { 328 | "description": "1 output, script provided, no change", 329 | "feeRate": 10, 330 | "inputs": [ 331 | 100000 332 | ], 333 | "outputs": [ 334 | { 335 | "script": { 336 | "length": 200 337 | }, 338 | "value": 95000 339 | } 340 | ], 341 | "expected": { 342 | "inputs": [ 343 | { 344 | "i": 0, 345 | "value": 100000 346 | } 347 | ], 348 | "outputs": [ 349 | { 350 | "script": { 351 | "length": 200 352 | }, 353 | "value": 95000 354 | } 355 | ], 356 | "fee": 5000 357 | } 358 | }, 359 | { 360 | "description": "1 output, script provided, change expected", 361 | "feeRate": 10, 362 | "inputs": [ 363 | 200000 364 | ], 365 | "outputs": [ 366 | { 367 | "script": { 368 | "length": 200 369 | }, 370 | "value": 95000 371 | } 372 | ], 373 | "expected": { 374 | "inputs": [ 375 | { 376 | "i": 0, 377 | "value": 200000 378 | } 379 | ], 380 | "outputs": [ 381 | { 382 | "script": { 383 | "length": 200 384 | }, 385 | "value": 95000 386 | }, 387 | { 388 | "value": 100990 389 | } 390 | ], 391 | "fee": 4010 392 | } 393 | }, 394 | { 395 | "description": "1 output, 2 inputs (related), no change", 396 | "feeRate": 10, 397 | "inputs": [ 398 | { 399 | "address": "a", 400 | "value": 100000 401 | }, 402 | { 403 | "address": "a", 404 | "value": 2000 405 | } 406 | ], 407 | "outputs": [ 408 | 98000 409 | ], 410 | "expected": { 411 | "inputs": [ 412 | { 413 | "i": 0, 414 | "address": "a", 415 | "value": 100000 416 | } 417 | ], 418 | "outputs": [ 419 | { 420 | "value": 98000 421 | } 422 | ], 423 | "fee": 2000 424 | } 425 | }, 426 | { 427 | "description": "many outputs, no change", 428 | "feeRate": 10, 429 | "inputs": [ 430 | 30000, 431 | 12220, 432 | 10001 433 | ], 434 | "outputs": [ 435 | 35000, 436 | 5000, 437 | 5000, 438 | 1000 439 | ], 440 | "expected": { 441 | "inputs": [ 442 | { 443 | "i": 0, 444 | "value": 30000 445 | }, 446 | { 447 | "i": 1, 448 | "value": 12220 449 | }, 450 | { 451 | "i": 2, 452 | "value": 10001 453 | } 454 | ], 455 | "outputs": [ 456 | { 457 | "value": 35000 458 | }, 459 | { 460 | "value": 5000 461 | }, 462 | { 463 | "value": 5000 464 | }, 465 | { 466 | "value": 1000 467 | } 468 | ], 469 | "fee": 6221 470 | } 471 | }, 472 | { 473 | "description": "many outputs, change expected", 474 | "feeRate": 10, 475 | "inputs": [ 476 | 30000, 477 | 14220, 478 | 10001 479 | ], 480 | "outputs": [ 481 | 35000, 482 | 5000, 483 | 5000, 484 | 1000 485 | ], 486 | "expected": { 487 | "inputs": [ 488 | { 489 | "i": 0, 490 | "value": 30000 491 | }, 492 | { 493 | "i": 1, 494 | "value": 14220 495 | }, 496 | { 497 | "i": 2, 498 | "value": 10001 499 | } 500 | ], 501 | "outputs": [ 502 | { 503 | "value": 35000 504 | }, 505 | { 506 | "value": 5000 507 | }, 508 | { 509 | "value": 5000 510 | }, 511 | { 512 | "value": 1000 513 | }, 514 | { 515 | "value": 1981 516 | } 517 | ], 518 | "fee": 6240 519 | } 520 | }, 521 | { 522 | "description": "many outputs, no fee, change expected", 523 | "feeRate": 0, 524 | "inputs": [ 525 | 5000, 526 | 5000, 527 | 5000, 528 | 5000, 529 | 5000, 530 | 5000 531 | ], 532 | "outputs": [ 533 | 28000, 534 | 1000 535 | ], 536 | "expected": { 537 | "inputs": [ 538 | { 539 | "i": 0, 540 | "value": 5000 541 | }, 542 | { 543 | "i": 1, 544 | "value": 5000 545 | }, 546 | { 547 | "i": 2, 548 | "value": 5000 549 | }, 550 | { 551 | "i": 3, 552 | "value": 5000 553 | }, 554 | { 555 | "i": 4, 556 | "value": 5000 557 | }, 558 | { 559 | "i": 5, 560 | "value": 5000 561 | } 562 | ], 563 | "outputs": [ 564 | { 565 | "value": 28000 566 | }, 567 | { 568 | "value": 1000 569 | }, 570 | { 571 | "value": 1000 572 | } 573 | ], 574 | "fee": 0 575 | } 576 | }, 577 | { 578 | "description": "no outputs, no change", 579 | "feeRate": 10, 580 | "inputs": [ 581 | 1900 582 | ], 583 | "outputs": [], 584 | "expected": { 585 | "inputs": [ 586 | { 587 | "i": 0, 588 | "value": 1900 589 | } 590 | ], 591 | "outputs": [], 592 | "fee": 1900 593 | } 594 | }, 595 | { 596 | "description": "no outputs, change expected", 597 | "feeRate": 10, 598 | "inputs": [ 599 | 20000 600 | ], 601 | "outputs": [], 602 | "expected": { 603 | "inputs": [ 604 | { 605 | "i": 0, 606 | "value": 20000 607 | } 608 | ], 609 | "outputs": [ 610 | { 611 | "value": 18080 612 | } 613 | ], 614 | "fee": 1920 615 | } 616 | }, 617 | { 618 | "description": "inputs used in order of DESCENDING", 619 | "feeRate": 10, 620 | "inputs": [ 621 | 20000, 622 | { 623 | "script": { 624 | "length": 300 625 | }, 626 | "value": 10000 627 | }, 628 | 10000 629 | ], 630 | "outputs": [ 631 | 25000 632 | ], 633 | "expected": { 634 | "fee": 5000, 635 | "inputs": [ 636 | { 637 | "i": 0, 638 | "value": 20000 639 | }, 640 | { 641 | "i": 2, 642 | "value": 10000 643 | } 644 | ], 645 | "outputs": [ 646 | { 647 | "value": 25000 648 | } 649 | ] 650 | } 651 | }, 652 | { 653 | "description": "not enough funds, empty result", 654 | "feeRate": 10, 655 | "inputs": [ 656 | 20000 657 | ], 658 | "outputs": [ 659 | 40000 660 | ], 661 | "expected": { 662 | "fee": 1920 663 | } 664 | }, 665 | { 666 | "description": "not enough funds (w/ fee), empty result", 667 | "feeRate": 10, 668 | "inputs": [ 669 | 40000 670 | ], 671 | "outputs": [ 672 | 40000 673 | ], 674 | "expected": { 675 | "fee": 1920 676 | } 677 | }, 678 | { 679 | "description": "not enough funds (no inputs), empty result", 680 | "feeRate": 10, 681 | "inputs": [], 682 | "outputs": [], 683 | "expected": { 684 | "fee": 100 685 | } 686 | }, 687 | { 688 | "description": "not enough funds (no inputs), empty result (>1KiB)", 689 | "feeRate": 10, 690 | "inputs": [], 691 | "outputs": [ 692 | 1, 693 | 1, 694 | 1, 695 | 1, 696 | 1, 697 | 1, 698 | 1, 699 | 1, 700 | 1, 701 | 1, 702 | 1, 703 | 1, 704 | 1, 705 | 1, 706 | 1, 707 | 1, 708 | 1, 709 | 1, 710 | 1, 711 | 1, 712 | 1, 713 | 1, 714 | 1, 715 | 1, 716 | 1, 717 | 1, 718 | 1, 719 | 1, 720 | 1 721 | ], 722 | "expected": { 723 | "fee": 9960 724 | } 725 | }, 726 | { 727 | "description": "2 outputs, some with missing value (NaN)", 728 | "feeRate": 10, 729 | "inputs": [ 730 | 20000 731 | ], 732 | "outputs": [ 733 | 1000, 734 | {} 735 | ], 736 | "expected": { 737 | "fee": 2260 738 | } 739 | }, 740 | { 741 | "description": "input with float values (NaN)", 742 | "feeRate": 10, 743 | "inputs": [ 744 | 20000.5 745 | ], 746 | "outputs": [ 747 | 10000, 748 | 1200 749 | ], 750 | "expected": { 751 | "fee": 2260 752 | } 753 | }, 754 | { 755 | "description": "2 outputs, with float values (NaN)", 756 | "feeRate": 10, 757 | "inputs": [ 758 | 20000 759 | ], 760 | "outputs": [ 761 | 10000.25, 762 | 1200.5 763 | ], 764 | "expected": { 765 | "fee": 2260 766 | } 767 | }, 768 | { 769 | "description": "2 outputs, string values (NaN)", 770 | "feeRate": 10, 771 | "inputs": [ 772 | 20000 773 | ], 774 | "outputs": [ 775 | { 776 | "value": "100" 777 | }, 778 | { 779 | "value": "204" 780 | } 781 | ], 782 | "expected": { 783 | "fee": 2260 784 | } 785 | }, 786 | { 787 | "description": "inputs and outputs, bad feeRate (NaN)", 788 | "feeRate": "1", 789 | "inputs": [ 790 | 20000 791 | ], 792 | "outputs": [ 793 | 10000 794 | ], 795 | "expected": {} 796 | }, 797 | { 798 | "description": "inputs and outputs, bad feeRate (NaN)", 799 | "feeRate": 1.5, 800 | "inputs": [ 801 | 20000 802 | ], 803 | "outputs": [ 804 | 10000 805 | ], 806 | "expected": {} 807 | } 808 | ] 809 | 810 | -------------------------------------------------------------------------------- /test/fixtures/split.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "1 to 3", 4 | "feeRate": 10, 5 | "inputs": [ 6 | 18000 7 | ], 8 | "outputs": [ 9 | {}, 10 | {}, 11 | {} 12 | ], 13 | "expected": { 14 | "inputs": [ 15 | { 16 | "value": 18000 17 | } 18 | ], 19 | "outputs": [ 20 | { 21 | "value": 5133 22 | }, 23 | { 24 | "value": 5133 25 | }, 26 | { 27 | "value": 5133 28 | } 29 | ], 30 | "fee": 2601 31 | } 32 | }, 33 | { 34 | "description": "5 to 2", 35 | "feeRate": 10, 36 | "inputs": [ 37 | 10000, 38 | 10000, 39 | 10000, 40 | 10000, 41 | 10000 42 | ], 43 | "outputs": [ 44 | {}, 45 | {} 46 | ], 47 | "expected": { 48 | "inputs": [ 49 | { 50 | "value": 10000 51 | }, 52 | { 53 | "value": 10000 54 | }, 55 | { 56 | "value": 10000 57 | }, 58 | { 59 | "value": 10000 60 | }, 61 | { 62 | "value": 10000 63 | } 64 | ], 65 | "outputs": [ 66 | { 67 | "value": 20910 68 | }, 69 | { 70 | "value": 20910 71 | } 72 | ], 73 | "fee": 8180 74 | } 75 | }, 76 | { 77 | "description": "3 to 1", 78 | "feeRate": 10, 79 | "inputs": [ 80 | 10000, 81 | 10000, 82 | 10000 83 | ], 84 | "outputs": [ 85 | {} 86 | ], 87 | "expected": { 88 | "inputs": [ 89 | { 90 | "value": 10000 91 | }, 92 | { 93 | "value": 10000 94 | }, 95 | { 96 | "value": 10000 97 | } 98 | ], 99 | "outputs": [ 100 | { 101 | "value": 25120 102 | } 103 | ], 104 | "fee": 4880 105 | } 106 | }, 107 | { 108 | "description": "3 to 1 (1 zero value output script)", 109 | "feeRate": 10, 110 | "inputs": [ 111 | 10000, 112 | 10000, 113 | 10000 114 | ], 115 | "outputs": [ 116 | {}, 117 | {"value": 0, "script": "foobar"} 118 | ], 119 | "expected": { 120 | "inputs": [ 121 | { 122 | "value": 10000 123 | }, 124 | { 125 | "value": 10000 126 | }, 127 | { 128 | "value": 10000 129 | } 130 | ], 131 | "outputs": [ 132 | { 133 | "value": 24970 134 | }, 135 | { 136 | "value": 0, 137 | "script": "foobar" 138 | } 139 | ], 140 | "fee": 5030 141 | } 142 | }, 143 | { 144 | "description": "3 to 3 (1 output pre-defined)", 145 | "feeRate": 10, 146 | "inputs": [ 147 | 10000, 148 | 10000, 149 | 10000 150 | ], 151 | "outputs": [ 152 | { 153 | "address": "foobar", 154 | "value": 12000 155 | }, 156 | { 157 | "address": "fizzbuzz" 158 | }, 159 | {} 160 | ], 161 | "expected": { 162 | "inputs": [ 163 | { 164 | "value": 10000 165 | }, 166 | { 167 | "value": 10000 168 | }, 169 | { 170 | "value": 10000 171 | } 172 | ], 173 | "outputs": [ 174 | { 175 | "address": "foobar", 176 | "value": 12000 177 | }, 178 | { 179 | "address": "fizzbuzz", 180 | "value": 6220 181 | }, 182 | { 183 | "value": 6220 184 | } 185 | ], 186 | "fee": 5560 187 | } 188 | }, 189 | { 190 | "description": "2 to 0 (no result)", 191 | "feeRate": 10, 192 | "inputs": [ 193 | 10000, 194 | 10000 195 | ], 196 | "outputs": [], 197 | "expected": { 198 | "fee": 3060 199 | } 200 | }, 201 | { 202 | "description": "0 to 2 (no result)", 203 | "feeRate": 10, 204 | "inputs": [], 205 | "outputs": [ 206 | {}, 207 | {} 208 | ], 209 | "expected": { 210 | "fee": 780 211 | } 212 | }, 213 | { 214 | "description": "1 to 2, output is dust (no result)", 215 | "feeRate": 10, 216 | "inputs": [ 217 | 2000 218 | ], 219 | "outputs": [ 220 | {} 221 | ], 222 | "expected": { 223 | "fee": 1920 224 | } 225 | }, 226 | { 227 | "description": "2 outputs, some with missing value (NaN)", 228 | "feeRate": 11, 229 | "inputs": [ 230 | 20000 231 | ], 232 | "outputs": [ 233 | { 234 | "value": 4000 235 | }, 236 | {} 237 | ], 238 | "expected": { 239 | "inputs": [ 240 | { 241 | "value": 20000 242 | } 243 | ], 244 | "outputs": [ 245 | { 246 | "value": 4000 247 | }, 248 | { 249 | "value": 13514 250 | } 251 | ], 252 | "fee": 2486 253 | } 254 | }, 255 | { 256 | "description": "2 outputs, some with float values (NaN)", 257 | "feeRate": 10, 258 | "inputs": [ 259 | 20000 260 | ], 261 | "outputs": [ 262 | { 263 | "value": 4000.5 264 | }, 265 | {} 266 | ], 267 | "expected": { 268 | "fee": 2260 269 | } 270 | }, 271 | { 272 | "description": "2 outputs, string values (NaN)", 273 | "feeRate": 11, 274 | "inputs": [ 275 | 20000 276 | ], 277 | "outputs": [ 278 | { 279 | "value": "100" 280 | }, 281 | { 282 | "value": "204" 283 | } 284 | ], 285 | "expected": { 286 | "fee": 2486 287 | } 288 | }, 289 | { 290 | "description": "input with float values (NaN)", 291 | "feeRate": 10, 292 | "inputs": [ 293 | 20000.5 294 | ], 295 | "outputs": [ 296 | {}, 297 | {} 298 | ], 299 | "expected": { 300 | "fee": 2260 301 | } 302 | }, 303 | { 304 | "description": "inputs and outputs, bad feeRate (NaN)", 305 | "feeRate": "1", 306 | "inputs": [ 307 | 20000 308 | ], 309 | "outputs": [ 310 | {} 311 | ], 312 | "expected": {} 313 | }, 314 | { 315 | "description": "inputs and outputs, bad feeRate (NaN)", 316 | "feeRate": 1.5, 317 | "inputs": [ 318 | 20000 319 | ], 320 | "outputs": [ 321 | {} 322 | ], 323 | "expected": {} 324 | } 325 | ] 326 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var coinSelect = require('../') 2 | var fixtures = require('./fixtures') 3 | var tape = require('tape') 4 | var utils = require('./_utils') 5 | 6 | fixtures.forEach(function (f) { 7 | tape(f.description, function (t) { 8 | var inputs = utils.expand(f.inputs, true) 9 | var outputs = utils.expand(f.outputs) 10 | var actual = coinSelect(inputs, outputs, f.feeRate) 11 | 12 | t.same(actual, f.expected) 13 | if (actual.inputs) { 14 | var feedback = coinSelect(actual.inputs, actual.outputs, f.feeRate) 15 | t.same(feedback, f.expected) 16 | } 17 | 18 | t.end() 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/split.js: -------------------------------------------------------------------------------- 1 | var coinSplit = require('../split') 2 | var fixtures = require('./fixtures/split') 3 | var tape = require('tape') 4 | var utils = require('./_utils') 5 | 6 | fixtures.forEach(function (f) { 7 | tape(f.description, function (t) { 8 | var finputs = utils.expand(f.inputs) 9 | var foutputs = f.outputs.concat() 10 | var actual = coinSplit(finputs, foutputs, f.feeRate) 11 | 12 | t.same(actual, f.expected) 13 | if (actual.inputs) { 14 | var feedback = coinSplit(finputs, actual.outputs, f.feeRate) 15 | t.same(feedback, f.expected) 16 | } 17 | 18 | t.end() 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var utils = require('../utils') 3 | 4 | tape('utils', function (t) { 5 | t.test('uintOrNaN', function (t) { 6 | t.plan(8) 7 | 8 | t.equal(utils.uintOrNaN(1), 1) 9 | t.equal(isNaN(utils.uintOrNaN('')), true) 10 | t.equal(isNaN(utils.uintOrNaN(Infinity)), true) 11 | t.equal(isNaN(utils.uintOrNaN(NaN)), true) 12 | t.equal(isNaN(utils.uintOrNaN('1')), true) 13 | t.equal(isNaN(utils.uintOrNaN('1.1')), true) 14 | t.equal(isNaN(utils.uintOrNaN(1.1)), true) 15 | t.equal(isNaN(utils.uintOrNaN(-1)), true) 16 | }) 17 | 18 | t.end() 19 | }) 20 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | // baseline estimates, used to improve performance 2 | var TX_EMPTY_SIZE = 4 + 1 + 1 + 4 3 | var TX_INPUT_BASE = 32 + 4 + 1 + 4 4 | var TX_INPUT_PUBKEYHASH = 107 5 | var TX_OUTPUT_BASE = 8 + 1 6 | var TX_OUTPUT_PUBKEYHASH = 25 7 | 8 | function inputBytes (input) { 9 | return TX_INPUT_BASE + (input.script ? input.script.length : TX_INPUT_PUBKEYHASH) 10 | } 11 | 12 | function outputBytes (output) { 13 | return TX_OUTPUT_BASE + (output.script ? output.script.length : TX_OUTPUT_PUBKEYHASH) 14 | } 15 | 16 | function dustThreshold (output, feeRate) { 17 | /* ... classify the output for input estimate */ 18 | return inputBytes({}) * feeRate 19 | } 20 | 21 | function transactionBytes (inputs, outputs) { 22 | return TX_EMPTY_SIZE + 23 | inputs.reduce(function (a, x) { return a + inputBytes(x) }, 0) + 24 | outputs.reduce(function (a, x) { return a + outputBytes(x) }, 0) 25 | } 26 | 27 | function uintOrNaN (v) { 28 | if (typeof v !== 'number') return NaN 29 | if (!isFinite(v)) return NaN 30 | if (Math.floor(v) !== v) return NaN 31 | if (v < 0) return NaN 32 | return v 33 | } 34 | 35 | function sumForgiving (range) { 36 | return range.reduce(function (a, x) { return a + (isFinite(x.value) ? x.value : 0) }, 0) 37 | } 38 | 39 | function sumOrNaN (range) { 40 | return range.reduce(function (a, x) { return a + uintOrNaN(x.value) }, 0) 41 | } 42 | 43 | var BLANK_OUTPUT = outputBytes({}) 44 | 45 | function finalize (inputs, outputs, feeRate) { 46 | var bytesAccum = transactionBytes(inputs, outputs) 47 | var feeAfterExtraOutput = feeRate * (bytesAccum + BLANK_OUTPUT) 48 | var remainderAfterExtraOutput = sumOrNaN(inputs) - (sumOrNaN(outputs) + feeAfterExtraOutput) 49 | 50 | // is it worth a change output? 51 | if (remainderAfterExtraOutput > dustThreshold({}, feeRate)) { 52 | outputs = outputs.concat({ value: remainderAfterExtraOutput }) 53 | } 54 | 55 | var fee = sumOrNaN(inputs) - sumOrNaN(outputs) 56 | if (!isFinite(fee)) return { fee: feeRate * bytesAccum } 57 | 58 | return { 59 | inputs: inputs, 60 | outputs: outputs, 61 | fee: fee 62 | } 63 | } 64 | 65 | module.exports = { 66 | dustThreshold: dustThreshold, 67 | finalize: finalize, 68 | inputBytes: inputBytes, 69 | outputBytes: outputBytes, 70 | sumOrNaN: sumOrNaN, 71 | sumForgiving: sumForgiving, 72 | transactionBytes: transactionBytes, 73 | uintOrNaN: uintOrNaN 74 | } 75 | --------------------------------------------------------------------------------