Skip to content

Commit

Permalink
Merge pull request #1 from bitcoinerlab/maxFundsWithTargets
Browse files Browse the repository at this point in the history
Update `maxFunds` Algorithm to Accept Fixed-Value Targets
  • Loading branch information
landabaso authored Nov 30, 2023
2 parents 87de51e + 927ab52 commit 0381888
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 0381888

Please sign in to comment.