diff --git a/accumulative.js b/accumulative.js index 42fd28f..9e2389d 100644 --- a/accumulative.js +++ b/accumulative.js @@ -2,7 +2,7 @@ var utils = require('./utils') // 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, changeAddress) { if (!isFinite(utils.uintOrNaN(feeRate))) return {} var bytesAccum = utils.transactionBytes([], outputs) @@ -31,7 +31,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, changeAddress) } return { fee: feeRate * bytesAccum } diff --git a/blackjack.js b/blackjack.js index 316568b..1f417c5 100644 --- a/blackjack.js +++ b/blackjack.js @@ -2,7 +2,7 @@ var utils = require('./utils') // 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, changeAddress) { if (!isFinite(utils.uintOrNaN(feeRate))) return {} var bytesAccum = utils.transactionBytes([], outputs) @@ -28,7 +28,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, changeAddress) } return { fee: feeRate * bytesAccum } diff --git a/funding.d.ts b/funding.d.ts new file mode 100644 index 0000000..39287a1 --- /dev/null +++ b/funding.d.ts @@ -0,0 +1,9 @@ +// funding.d.ts +import { Target, UTXO } from "./index"; + +export default function funding(inputs: UTXO[], outputs: Target[], feeRate: number): { + funding: number, + totalOutput: number, + totalInput: number, + fee: number +}; \ No newline at end of file diff --git a/funding.js b/funding.js new file mode 100644 index 0000000..65fd837 --- /dev/null +++ b/funding.js @@ -0,0 +1,27 @@ +const utils = require('./utils') + +// given inputs, outputs and feerate, return sats needed to fund the transaction +module.exports = function funding (inputs, outputs, feeRate) { + if (!isFinite(utils.uintOrNaN(feeRate))) return {} + + let inAccum = 0 + const outAccum = utils.sumOrNaN(outputs) + + for (var i = 0; i < inputs.length; ++i) { + const utxo = inputs[i] + const utxoValue = utils.uintOrNaN(utxo.value) + + inAccum += utxoValue + } + + // use all inputs and outputs + const bytesAccum = utils.transactionBytes(inputs, outputs) + const fee = feeRate * bytesAccum + + return { + funding: Math.ceil(outAccum + fee - inAccum), // negative if overfunded + totalOutput: outAccum, + totalInput: inAccum, + fee: fee + } +} diff --git a/index.d.ts b/index.d.ts index 3abf5db..e1252e7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,4 +21,4 @@ export interface SelectedUTXO { outputs?: Target[], fee: number } -export default function coinSelect(utxos: UTXO[], outputs: Target[], feeRate: number): SelectedUTXO; +export default function coinSelect(utxos: UTXO[], outputs: Target[], feeRate: number, changeAddress?: string): SelectedUTXO; diff --git a/index.js b/index.js index 19aa484..5338811 100644 --- a/index.js +++ b/index.js @@ -7,15 +7,15 @@ 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, changeAddress) { 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, changeAddress) if (base.inputs) return base // else, try the accumulative strategy - return accumulative(utxos, outputs, feeRate) + return accumulative(utxos, outputs, feeRate, changeAddress) } diff --git a/package.json b/package.json index b02703f..0d6fa2a 100644 --- a/package.json +++ b/package.json @@ -26,13 +26,48 @@ "accumulative.js", "blackjack.js", "break.js", + "funding.js", + "funding.d.ts", "index.js", "split.js", "utils.js", + "utils.d.ts", "index.d.ts" ], "main": "index.js", "types": "index.d.ts", + "exports": { + ".": { + "import": "./index.js", + "require": "./index.js" + }, + "./accumulative": { + "import": "./accumulative.js", + "require": "./accumulative.js" + }, + "./blackjack": { + "import": "./blackjack.js", + "require": "./blackjack.js" + }, + "./break": { + "import": "./break.js", + "require": "./break.js" + }, + "./funding": { + "import": "./funding.js", + "require": "./funding.js", + "types": "./funding.d.ts" + }, + "./split": { + "import": "./split.js", + "require": "./split.js" + }, + "./utils": { + "import": "./utils.js", + "require": "./utils.js", + "types": "./utils.d.ts" + } + }, "repository": { "type": "git", "url": "https://github.com/ChrisCho-H/bitcoinselect.git" diff --git a/test/fixtures/funding.json b/test/fixtures/funding.json new file mode 100644 index 0000000..9e9af3a --- /dev/null +++ b/test/fixtures/funding.json @@ -0,0 +1,68 @@ +[ + { + "description": "valid funding with 1 input and 2 outputs", + "feeRate": 1, + "inputs": [ + 1000 + ], + "outputs": [ + 1000, + 600 + ], + "expected": { + "funding": 826, + "totalOutput": 1600, + "totalInput": 1000, + "fee": 226 + } + }, + { + "description": "valid funding with 1 input (+witnessUtxo) and 2 outputs", + "feeRate": 1, + "inputs": [ + { + "value": 1000, + "witnessUtxo": {} + } + ], + "outputs": [ + 1000, + 600 + ], + "expected": { + "funding": 746, + "totalOutput": 1600, + "totalInput": 1000, + "fee": 146 + } + }, + { + "description": "negative funding when inputs > outputs", + "feeRate": 1, + "inputs": [ + 2000 + ], + "outputs": [ + 1000, + 600 + ], + "expected": { + "funding": -174, + "totalOutput": 1600, + "totalInput": 2000, + "fee": 226 + } + }, + { + "description": "bad feerate", + "feeRate": 1.5, + "inputs": [ + 2000 + ], + "outputs": [ + 1000, + 600 + ], + "expected": {} + } +] \ No newline at end of file diff --git a/test/funding.js b/test/funding.js new file mode 100644 index 0000000..f9889ab --- /dev/null +++ b/test/funding.js @@ -0,0 +1,20 @@ +const fixtures = require('./fixtures/funding.json') +const utils = require('./_utils') +const tape = require('tape') +const funding = require('../funding') + +fixtures.forEach(function (f) { + tape(f.description, function (t) { + var inputs = utils.expand(f.inputs, true) + var outputs = utils.expand(f.outputs) + var actual = funding(inputs, outputs, f.feeRate) + + t.same(actual, f.expected) + if (actual.inputs) { + var feedback = funding(actual.inputs, actual.outputs, f.feeRate) + t.same(feedback, f.expected) + } + + t.end() + }) +}) diff --git a/utils.d.ts b/utils.d.ts new file mode 100644 index 0000000..0a8e5d7 --- /dev/null +++ b/utils.d.ts @@ -0,0 +1,13 @@ +// utils.d.ts +import {SelectedUTXO, Target, UTXO} from "./index"; + +declare module "bitcoinselect/utils" { + export function inputBytes(input: UTXO): number; + export function outputBytes(output: Target): number; + export function dustThreshold(output: Target, feeRate: number): number; + export function transactionBytes(inputs: UTXO[], outputs: Target[]): number; + export function uintOrNaN(v: any): number; + export function sumForgiving(range: any[]): number; + export function sumOrNaN(range: any[]): number; + export function finalize(inputs: UTXO[], outputs: Target[], feeRate: number, changeAddress?: string): SelectedUTXO; +} \ No newline at end of file diff --git a/utils.js b/utils.js index af55a19..d65a9bf 100644 --- a/utils.js +++ b/utils.js @@ -1,9 +1,9 @@ // baseline estimates, used to improve performance -var TX_EMPTY_SIZE = 4 + 1 + 1 + 4 -var TX_INPUT_BASE = 32 + 4 + 1 + 4 +var TX_EMPTY_SIZE = 4 + 1 + 1 + 4 + 0.5 // version + inputcount + outputcount + locktime + marker-flag (segwit) +var TX_INPUT_BASE = 32 + 4 + 1 + 4 // txid + vout + scriptsigsize + sequence var TX_INPUT_PUBKEYHASH = 107 var TX_INPUT_SEGWIT = 27 -var TX_INPUT_TAPROOT = 17 // round up 16.5 bytes +var TX_INPUT_TAPROOT = 16.5 // round up 16.5 bytes - witness sig for key path spend var TX_OUTPUT_BASE = 8 + 1 var TX_OUTPUT_PUBKEYHASH = 25 var TX_OUTPUT_SCRIPTHASH = 23 @@ -20,8 +20,8 @@ function inputBytes (input) { function outputBytes (output) { return TX_OUTPUT_BASE + (output.script ? output.script.length - : output.address?.startsWith('bc1') || output.address?.startsWith('tb1') - ? output.address?.length === 42 ? TX_OUTPUT_SEGWIT : TX_OUTPUT_SEGWIT_SCRIPTHASH + : output.address?.startsWith('bc1') || output.address?.startsWith('tb1') || output.address?.startsWith('bcrt1') + ? output.address?.length === 42 || output.address?.length === 44 ? TX_OUTPUT_SEGWIT : TX_OUTPUT_SEGWIT_SCRIPTHASH : output.address?.startsWith('3') || output.address?.startsWith('2') ? TX_OUTPUT_SCRIPTHASH : TX_OUTPUT_PUBKEYHASH ) @@ -41,7 +41,6 @@ function transactionBytes (inputs, outputs) { function uintOrNaN (v) { if (typeof v !== 'number') return NaN if (!isFinite(v)) return NaN - if (Math.floor(v) !== v) return NaN if (v < 0) return NaN return v } @@ -56,14 +55,15 @@ function sumOrNaN (range) { var BLANK_OUTPUT = outputBytes({}) -function finalize (inputs, outputs, feeRate) { +function finalize (inputs, outputs, feeRate, changeAddress) { var bytesAccum = transactionBytes(inputs, outputs) - var feeAfterExtraOutput = feeRate * (bytesAccum + BLANK_OUTPUT) + const changeOutputBytes = changeAddress ? outputBytes({address:changeAddress}): BLANK_OUTPUT + var feeAfterExtraOutput = Math.ceil(feeRate * (bytesAccum + changeOutputBytes)) var remainderAfterExtraOutput = sumOrNaN(inputs) - (sumOrNaN(outputs) + feeAfterExtraOutput) // is it worth a change output? if (remainderAfterExtraOutput > dustThreshold({}, feeRate)) { - outputs = outputs.concat({ value: remainderAfterExtraOutput }) + outputs = outputs.concat({ value: Math.ceil(remainderAfterExtraOutput) }) } var fee = sumOrNaN(inputs) - sumOrNaN(outputs)