diff --git a/coinlib/lib/src/tx/coin_selection.dart b/coinlib/lib/src/tx/coin_selection.dart index 8b7357d..2ce4f0a 100644 --- a/coinlib/lib/src/tx/coin_selection.dart +++ b/coinlib/lib/src/tx/coin_selection.dart @@ -1,6 +1,7 @@ import 'package:coinlib/src/common/serial.dart'; import 'package:coinlib/src/crypto/random.dart'; import 'package:coinlib/src/scripts/program.dart'; +import 'package:collection/collection.dart'; import 'inputs/input.dart'; import 'output.dart'; import 'transaction.dart'; @@ -122,6 +123,49 @@ class CoinSelection { } + /// A simple selection algorithm that selects inputs from the [candidates] + /// starting from the largest value until the required amount has been + /// reached and then the selected inputs are given a random order. + factory CoinSelection.largestFirst({ + int version = Transaction.currentVersion, + required Iterable candidates, + required Iterable recipients, + required Program changeProgram, + required BigInt feePerKb, + required BigInt minFee, + required BigInt minChange, + int locktime = 0, + }) { + + final sorted = candidates.toList().sorted( + (a, b) => b.value.compareTo(a.value), + ); + + CoinSelection trySelection(Iterable selected) + => CoinSelection( + version: version, + selected: selected, + recipients: recipients, + changeProgram: changeProgram, + feePerKb: feePerKb, + minFee: minFee, + minChange: minChange, + locktime: locktime, + ); + + CoinSelection selection = trySelection([]); + for (int i = 0; i < sorted.length; i++) { + selection = trySelection(sorted.take(i+1)); + if (selection.enoughFunds) break; + } + + // Randomising makes it less obvious this algo is being used + final selected = selection.selected.toList(); + selected.shuffle(); + return trySelection(selected); + + } + /// Obtains the transaction with selected inputs and outputs including any /// change at a random location, ready to be signed. Throws /// [InsufficientFunds] if there is not enough input value to meet the output @@ -147,7 +191,7 @@ class CoinSelection { bool get changeless => changeValue.compareTo(BigInt.zero) == 0; /// True if the resulting fully signed transaction will be too large bool get tooLarge => signedSize > Transaction.maxSize; - /// True if a signable solution have been found + /// True if a signable solution has been found bool get ready => enoughFunds && !tooLarge; } diff --git a/coinlib/test/tx/coin_selection_test.dart b/coinlib/test/tx/coin_selection_test.dart index c4719c8..89364f2 100644 --- a/coinlib/test/tx/coin_selection_test.dart +++ b/coinlib/test/tx/coin_selection_test.dart @@ -272,6 +272,40 @@ void main() { }); + void expectSelectedValues(CoinSelection selection, List values) => expect( + selection.selected.map((candidate) => candidate.value.toInt()), + unorderedEquals(values), + ); + + test(".largestFirst()", () { + + final candidates = [coin*4, coin, coin*3, coin, coin*2]; + + void expectLargestFirst(List selected, int outValue) { + final selection = CoinSelection.largestFirst( + version: 1234, + candidates: candidates.map((value) => candidateForValue(value)), + recipients: [outputForValue(outValue)], + changeProgram: changeProgram, + feePerKb: feePerKb, minFee: minFee, minChange: minChange, + locktime: 0xabcd1234, + ); + expectSelectedValues(selection, selected); + expect(selection.version, 1234); + expect(selection.locktime, 0xabcd1234); + } + + // Can cover with single largest + expectLargestFirst([coin*4], coin*3); + // Can cover with two + expectLargestFirst([coin*4, coin*3], coin*4); + // Need all + expectLargestFirst(candidates, coin*10); + // Select all, though they aren't enough + expectLargestFirst(candidates, coin*12); + + }); + }); }