Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions accumulative.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 }
Expand Down
4 changes: 2 additions & 2 deletions blackjack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 }
Expand Down
9 changes: 9 additions & 0 deletions funding.d.ts
Original file line number Diff line number Diff line change
@@ -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
};
27 changes: 27 additions & 0 deletions funding.js
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
6 changes: 3 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
35 changes: 35 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
68 changes: 68 additions & 0 deletions test/fixtures/funding.json
Original file line number Diff line number Diff line change
@@ -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": {}
}
]
20 changes: 20 additions & 0 deletions test/funding.js
Original file line number Diff line number Diff line change
@@ -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()
})
})
13 changes: 13 additions & 0 deletions utils.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
18 changes: 9 additions & 9 deletions utils.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
)
Expand All @@ -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
}
Expand All @@ -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)
Expand Down