From 03de6f3654d58b9ca0c56fefc7a5ad09469b7286 Mon Sep 17 00:00:00 2001 From: KevinK Date: Tue, 7 Jan 2025 15:45:00 +0800 Subject: [PATCH 1/2] add funding function + fix estimate --- accumulative.js | 4 +-- blackjack.js | 4 +-- funding.d.ts | 9 +++++ funding.js | 27 +++++++++++++++ index.d.ts | 2 +- index.js | 6 ++-- package.json | 35 ++++++++++++++++++++ test/fixtures/funding.json | 68 ++++++++++++++++++++++++++++++++++++++ test/funding.js | 20 +++++++++++ utils.d.ts | 13 ++++++++ utils.js | 18 +++++----- 11 files changed, 189 insertions(+), 17 deletions(-) create mode 100644 funding.d.ts create mode 100644 funding.js create mode 100644 test/fixtures/funding.json create mode 100644 test/funding.js create mode 100644 utils.d.ts 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..7243322 --- /dev/null +++ b/funding.d.ts @@ -0,0 +1,9 @@ +// funding.d.ts +import { Target, UTXO } from "./index"; + +export default function funding(utxos: 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..881dd91 --- /dev/null +++ b/funding.js @@ -0,0 +1,27 @@ +const utils = require('./utils') + +// given utxos, outputs and feerate, return sats needed to fund the transaction +module.exports = function funding (utxos, outputs, feeRate) { + if (!isFinite(utils.uintOrNaN(feeRate))) return {} + + let inAccum = 0 + const outAccum = utils.sumOrNaN(outputs) + + for (var i = 0; i < utxos.length; ++i) { + const utxo = utxos[i] + const utxoValue = utils.uintOrNaN(utxo.value) + + inAccum += utxoValue + } + + // use all utxos as inputs + const bytesAccum = utils.transactionBytes(utxos, 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) From 72537a9327b9399cdca3eb148f93712a0c7a127c Mon Sep 17 00:00:00 2001 From: KevinK Date: Thu, 9 Jan 2025 13:58:26 +0800 Subject: [PATCH 2/2] rename utxos param to inputs --- funding.d.ts | 2 +- funding.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/funding.d.ts b/funding.d.ts index 7243322..39287a1 100644 --- a/funding.d.ts +++ b/funding.d.ts @@ -1,7 +1,7 @@ // funding.d.ts import { Target, UTXO } from "./index"; -export default function funding(utxos: UTXO[], outputs: Target[], feeRate: number): { +export default function funding(inputs: UTXO[], outputs: Target[], feeRate: number): { funding: number, totalOutput: number, totalInput: number, diff --git a/funding.js b/funding.js index 881dd91..65fd837 100644 --- a/funding.js +++ b/funding.js @@ -1,21 +1,21 @@ const utils = require('./utils') -// given utxos, outputs and feerate, return sats needed to fund the transaction -module.exports = function funding (utxos, outputs, feeRate) { +// 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 < utxos.length; ++i) { - const utxo = utxos[i] + for (var i = 0; i < inputs.length; ++i) { + const utxo = inputs[i] const utxoValue = utils.uintOrNaN(utxo.value) inAccum += utxoValue } - // use all utxos as inputs - const bytesAccum = utils.transactionBytes(utxos, outputs) + // use all inputs and outputs + const bytesAccum = utils.transactionBytes(inputs, outputs) const fee = feeRate * bytesAccum return {