Skip to content

Commit

Permalink
Update maxFunds Algorithm to Accept Fixed-Value Targets
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
landabaso committed Nov 30, 2023
1 parent 87de51e commit 927ab52
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 73 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)' }),
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
20 changes: 14 additions & 6 deletions src/algos/maxFunds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OutputWithValue>;
targets: Array<OutputWithValue>;
/**
* Recipient to send maxFunds
*/
Expand All @@ -35,22 +38,26 @@ 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
)
);

// Only consider inputs with more value than the fee they require
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);
Expand All @@ -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,
Expand Down
58 changes: 24 additions & 34 deletions src/coinselect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -87,7 +82,7 @@ export function coinselect({
* Array of transaction targets. If specified, `remainder` is used
* as the change address.
*/
targets?: Array<OutputWithValue>;
targets: Array<OutputWithValue>;
/**
* `OutputInstance` used as the change address when targets are specified,
* or as the recipient address for maximum fund transactions.
Expand Down Expand Up @@ -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
Expand Down
50 changes: 29 additions & 21 deletions test/coinselect.test.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -20,35 +20,43 @@ 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
// from bitcoinjs/coinselect which operate like this:
// 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(
// {
Expand Down
58 changes: 53 additions & 5 deletions test/fixtures/maxFunds.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand All @@ -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": []
}
]

0 comments on commit 927ab52

Please sign in to comment.