From 927ab523d74b382ad761f51ef6b3d38db74e6ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Landabaso=20D=C3=ADaz?= Date: Thu, 30 Nov 2023 08:42:06 +0100 Subject: [PATCH] Update `maxFunds` Algorithm to Accept Fixed-Value Targets This update to the `maxFunds` algorithm in the @bitcoinerlab/coinselect library enhances its functionality by allowing the inclusion of fixed-value targets in addition to the remainder. The change ensures more flexibility in handling scenarios where a combination of specific targets and the maximum possible fund transfer to a recipient is desired. The README.md and relevant test fixtures have been updated to reflect this change, and the package version has been bumped to 1.1.0 to indicate this significant update. Key Changes: - Modified `maxFunds` function signature to include an additional `targets` parameter. - Updated README.md with new usage examples and descriptions. - Adjusted test fixtures to cover the updated functionality of `maxFunds`. - Version bump in package.json and package-lock.json to 1.1.0. These changes enhance the utility of the `maxFunds` algorithm, aligning it with practical use cases in Bitcoin transaction management. --- README.md | 13 ++++++--- package-lock.json | 4 +-- package.json | 2 +- src/algos/maxFunds.ts | 20 +++++++++---- src/coinselect.ts | 58 +++++++++++++++---------------------- test/coinselect.test.ts | 50 ++++++++++++++++++-------------- test/fixtures/maxFunds.json | 58 +++++++++++++++++++++++++++++++++---- 7 files changed, 132 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index f794f05..237df3e 100644 --- a/README.md +++ b/README.md @@ -154,13 +154,15 @@ Note that in the selection process, each UTXO's contribution towards the transac ### Sending Max Funds -The `maxFunds` algorithm is tailored for situations where the aim is to transfer all funds from available UTXOs to a single recipient address. To utilize this functionality, either directly import and use `maxFunds` or apply `coinselect` by specifying the recipient's address in the `remainder` argument while omitting the `targets`. This approach ensures that all available funds, minus the transaction fees, are sent to the specified recipient address. +The `maxFunds` algorithm is ideal for transferring all available funds from UTXOs to a specified recipient. To use this algorithm, specify the recipient in the `remainder`. It's also possible to set additional fixed-value targets, if needed. + +If the `remainder` value would be below the dust threshold, the function returns `undefined`. Example: ```typescript -import { coinselect } from '@bitcoinerlab/coinselect'; +import { maxFunds } from '@bitcoinerlab/coinselect'; -const { utxos, targets, fee, vsize } = coinselect({ +const { utxos, targets, fee, vsize } = maxFunds({ utxos: [ { output: new Output({ descriptor: 'addr(bc1qzne9qykh9j55qt8ccqamusp099spdfr49tje60)' }), @@ -171,12 +173,15 @@ const { utxos, targets, fee, vsize } = coinselect({ value: 4000 } ], + targets: [ + // Additional fixed-value targets can be included here + ], remainder: new Output({ descriptor: 'addr(bc1qwfh5mj2kms4rrf8amr66f7d5ckmpdqdzlpr082)' }), feeRate: 1.34 }); ``` -The final recipient value in the transaction will be: `targets[0].value`. +The value for the recipient (remainder) is determined by subtracting the sum of the values in `targets` and the `fee` from the total value of `utxos`. To access the recipient's value, use `targets[targets.length - 1].value`. ### Avoid Change diff --git a/package-lock.json b/package-lock.json index 9150f6a..028e4bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitcoinerlab/coinselect", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@bitcoinerlab/coinselect", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "dependencies": { "@bitcoinerlab/descriptors": "^2.0.3" diff --git a/package.json b/package.json index 6856224..884d483 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitcoinerlab/coinselect", - "version": "1.0.0", + "version": "1.1.0", "author": "Jose-Luis Landabaso", "license": "MIT", "description": "A TypeScript library for Bitcoin transaction management, based on Bitcoin Descriptors for defining inputs and outputs. It facilitates optimal UTXO selection and transaction size calculation.", diff --git a/src/algos/maxFunds.ts b/src/algos/maxFunds.ts index 0749b21..a12b8d8 100644 --- a/src/algos/maxFunds.ts +++ b/src/algos/maxFunds.ts @@ -10,23 +10,26 @@ import { isDust } from '../dust'; /** * The `maxFunds` algorithm is tailored for scenarios where the goal is to transfer all funds from specified UTXOs to a single recipient output. - * To utilize this function, specify the recipient output in the `remainder` argument, while omitting the `targets` parameter. + * To utilize this function, specify the recipient output in the `remainder` argument. * In this context, the `remainder` serves as the recipient of the funds. * * Notes: * * - This function does not reorder UTXOs prior to selection. * - UTXOs that do not provide enough value to cover their respective fee contributions are automatically excluded. + * - Recipient of all funds is set to last position of the returned `targets` array. * * Refer to {@link coinselect coinselect} for additional details on input parameters and expected returned values. */ export function maxFunds({ utxos, + targets, remainder, feeRate, dustRelayFeeRate = DUST_RELAY_FEE_RATE }: { utxos: Array; + targets: Array; /** * Recipient to send maxFunds */ @@ -35,14 +38,18 @@ export function maxFunds({ dustRelayFeeRate?: number; }) { validateOutputWithValues(utxos); + targets.length === 0 || validateOutputWithValues(targets); validateFeeRate(feeRate); validateFeeRate(dustRelayFeeRate); + const outputs = [...targets.map(target => target.output), remainder]; + const targetsValue = targets.reduce((a, target) => a + target.value, 0); + const allUtxosFee = Math.ceil( feeRate * vsize( utxos.map(utxo => utxo.output), - [remainder] + outputs ) ); @@ -50,7 +57,7 @@ export function maxFunds({ const validUtxos = utxos.filter(validUtxo => { const txSizeWithoutUtxo = vsize( utxos.filter(utxo => utxo !== validUtxo).map(utxo => utxo.output), - [remainder] + outputs ); const feeContribution = allUtxosFee - Math.ceil(feeRate * txSizeWithoutUtxo); @@ -62,15 +69,16 @@ export function maxFunds({ feeRate * vsize( validUtxos.map(utxo => utxo.output), - [remainder] + outputs ) ); const validUtxosValue = validUtxos.reduce((a, utxo) => a + utxo.value, 0); - const remainderValue = validUtxosValue - validFee; + const remainderValue = validUtxosValue - targetsValue - validFee; if (!isDust(remainder, remainderValue, dustRelayFeeRate)) { //return the same reference if nothing changed to interact nicely with //reactive components - const targets = [{ output: remainder, value: remainderValue }]; + //mutate targets: + targets = [...targets, { output: remainder, value: remainderValue }]; return { utxos: utxos.length === validUtxos.length ? utxos : validUtxos, targets, diff --git a/src/coinselect.ts b/src/coinselect.ts index 0740159..3ca64f9 100644 --- a/src/coinselect.ts +++ b/src/coinselect.ts @@ -8,7 +8,6 @@ import { } from './validation'; import { addUntilReach } from './algos/addUntilReach'; import { avoidChange } from './algos/avoidChange'; -import { maxFunds } from './algos/maxFunds'; import { inputWeight } from './vsize'; import { isSegwitTx } from './segwit'; @@ -38,10 +37,6 @@ function utxoTransferredValue( * until the total value exceeds the target value plus fees. * Change is added only if it's above the {@link dustThreshold dustThreshold}). * - * To transfer all funds from your UTXOs to a recipient address, specify the - * recipient in the `remainder` argument and omit the `targets`. This way, - * the {@link maxFunds maxFunds} algorithm is used. - * * UTXOs that do not provide enough value to cover their respective fee * contributions are automatically excluded. * @@ -87,7 +82,7 @@ export function coinselect({ * Array of transaction targets. If specified, `remainder` is used * as the change address. */ - targets?: Array; + targets: Array; /** * `OutputInstance` used as the change address when targets are specified, * or as the recipient address for maximum fund transactions. @@ -117,34 +112,29 @@ export function coinselect({ //Note that having one segwit utxo does not mean the final tx will be segwit //(because the coinselect algo may end up choosing only non-segwit utxos). - let coinselected; - if (targets) { - const isPossiblySegwitTx = isSegwitTx(utxos.map(utxo => utxo.output)); - //Sort in descending utxoTransferredValue - //Using [...utxos] because sort mutates the input - const sortedUtxos = [...utxos].sort( - (a, b) => - utxoTransferredValue(b, feeRate, isPossiblySegwitTx) - - utxoTransferredValue(a, feeRate, isPossiblySegwitTx) - ); - coinselected = - avoidChange({ - utxos: sortedUtxos, - targets, - remainder, - feeRate, - dustRelayFeeRate - }) || - addUntilReach({ - utxos: sortedUtxos, - targets, - remainder, - feeRate, - dustRelayFeeRate - }); - } else { - coinselected = maxFunds({ utxos, remainder, feeRate, dustRelayFeeRate }); - } + const isPossiblySegwitTx = isSegwitTx(utxos.map(utxo => utxo.output)); + //Sort in descending utxoTransferredValue + //Using [...utxos] because sort mutates the input + const sortedUtxos = [...utxos].sort( + (a, b) => + utxoTransferredValue(b, feeRate, isPossiblySegwitTx) - + utxoTransferredValue(a, feeRate, isPossiblySegwitTx) + ); + const coinselected = + avoidChange({ + utxos: sortedUtxos, + targets, + remainder, + feeRate, + dustRelayFeeRate + }) || + addUntilReach({ + utxos: sortedUtxos, + targets, + remainder, + feeRate, + dustRelayFeeRate + }); if (coinselected) { //return the same reference if nothing changed to interact nicely with //reactive components diff --git a/test/coinselect.test.ts b/test/coinselect.test.ts index d6fb45f..b97709d 100644 --- a/test/coinselect.test.ts +++ b/test/coinselect.test.ts @@ -1,4 +1,4 @@ -import { coinselect, addUntilReach } from '../dist'; +import { coinselect, addUntilReach, maxFunds } from '../dist'; import * as secp256k1 from '@bitcoinerlab/secp256k1'; import { DescriptorsFactory } from '@bitcoinerlab/descriptors'; const { Output } = DescriptorsFactory(secp256k1); @@ -20,18 +20,15 @@ for (const fixturesWithDescription of [ value: utxo.value, output: new Output({ descriptor: utxo.descriptor }) })); - const targets = - 'targets' in fixture && - Array.isArray(fixture.targets) && - fixture.targets.map(target => ({ - value: target.value, - output: new Output({ descriptor: target.descriptor }) - })); + const targets = fixture.targets.map(target => ({ + value: target.value, + output: new Output({ descriptor: target.descriptor }) + })); const coinselected = - setDescription !== 'addUntilReach' - ? coinselect({ + setDescription === 'addUntilReach' + ? addUntilReach({ utxos, - ...(targets ? { targets } : {}), + targets, remainder: new Output({ descriptor: fixture.remainder }), feeRate: fixture.feeRate, // This is probably a bad idea, but we're m using tests fixtures @@ -39,16 +36,27 @@ for (const fixturesWithDescription of [ // https://github.com/bitcoinjs/coinselect/issues/86 dustRelayFeeRate: fixture.feeRate }) - : addUntilReach({ - utxos, - targets: targets || [], - remainder: new Output({ descriptor: fixture.remainder }), - feeRate: fixture.feeRate, - // This is probably a bad idea, but we're m using tests fixtures - // from bitcoinjs/coinselect which operate like this: - // https://github.com/bitcoinjs/coinselect/issues/86 - dustRelayFeeRate: fixture.feeRate - }); + : setDescription === 'maxFunds' + ? maxFunds({ + utxos, + targets, + remainder: new Output({ descriptor: fixture.remainder }), + feeRate: fixture.feeRate, + // This is probably a bad idea, but we're m using tests fixtures + // from bitcoinjs/coinselect which operate like this: + // https://github.com/bitcoinjs/coinselect/issues/86 + dustRelayFeeRate: fixture.feeRate + }) + : coinselect({ + utxos, + targets, + remainder: new Output({ descriptor: fixture.remainder }), + feeRate: fixture.feeRate, + // This is probably a bad idea, but we're m using tests fixtures + // from bitcoinjs/coinselect which operate like this: + // https://github.com/bitcoinjs/coinselect/issues/86 + dustRelayFeeRate: fixture.feeRate + }); //console.log( // JSON.stringify( // { diff --git a/test/fixtures/maxFunds.json b/test/fixtures/maxFunds.json index 51956e9..0f6bad4 100644 --- a/test/fixtures/maxFunds.json +++ b/test/fixtures/maxFunds.json @@ -22,8 +22,51 @@ { "value": 25120 } + ] + }, + "utxos": [ + { + "value": 10000, + "descriptor": "addr(12higDjoCCNXSA95xZMWUdPvXNmkAduhWv)" + }, + { + "value": 10000, + "descriptor": "addr(12higDjoCCNXSA95xZMWUdPvXNmkAduhWv)" + }, + { + "value": 10000, + "descriptor": "addr(12higDjoCCNXSA95xZMWUdPvXNmkAduhWv)" + } + ], + "targets": [] + }, + { + "description": "maxFunds 3 utxos, 1 target", + "remainder": "addr(12higDjoCCNXSA95xZMWUdPvXNmkAduhWv)", + "feeRate": 10, + "expected": { + "inputs": [ + { + "i": 0, + "value": 10000 + }, + { + "i": 1, + "value": 10000 + }, + { + "i": 2, + "value": 10000 + } ], - "fee": 4880 + "outputs": [ + { + "value": 1000 + }, + { + "value": 23780 + } + ] }, "utxos": [ { @@ -38,20 +81,25 @@ "value": 10000, "descriptor": "addr(12higDjoCCNXSA95xZMWUdPvXNmkAduhWv)" } + ], + "targets": [ + { + "value": 1000, + "descriptor": "addr(12higDjoCCNXSA95xZMWUdPvXNmkAduhWv)" + } ] }, { "description": "maxFunds, output is dust (no result)", "remainder": "addr(12higDjoCCNXSA95xZMWUdPvXNmkAduhWv)", "feeRate": 10, - "expected": { - "fee": 1920 - }, + "expected": {}, "utxos": [ { "value": 2000, "descriptor": "addr(12higDjoCCNXSA95xZMWUdPvXNmkAduhWv)" } - ] + ], + "targets": [] } ]