1- import 'package:coinlib/src/common/serial .dart' ;
1+ import 'package:coinlib/src/common/bigints .dart' ;
22import 'package:coinlib/src/crypto/random.dart' ;
33import 'package:coinlib/src/scripts/program.dart' ;
4- import 'package:coinlib/src/tx/inputs/witness_input.dart' ;
54import 'package:collection/collection.dart' ;
65import 'inputs/input.dart' ;
7- import 'inputs/taproot_input.dart' ;
86import 'locktime.dart' ;
97import 'output.dart' ;
108import 'transaction.dart' ;
@@ -18,20 +16,12 @@ class InputCandidate {
1816 final Input input;
1917 /// Value of UTXO to be spent
2018 final BigInt value;
21- /// True if it is known that the default sighash type is being used which
22- /// allows one less byte to be used for Taproot signatures.
23- final bool defaultSigHash;
2419
2520 /// Provides an [input] alongside the [value] being spent that may be
2621 /// selected.
27- ///
28- /// [defaultSigHash] can be set to true if it is known that a Taproot input
29- /// will definitely be signed with SIGHASH_DEFAULT. The fee calculation will
30- /// be incorrect if this is set for a non-default sighash type.
3122 InputCandidate ({
3223 required this .input,
3324 required this .value,
34- this .defaultSigHash = false ,
3525 });
3626
3727}
@@ -55,31 +45,19 @@ class CoinSelection {
5545 late final BigInt inputValue;
5646 /// The total value of all recipient outputs
5747 late final BigInt recipientValue;
58- /// The fee to be paid by the transaction
59- late final BigInt fee;
6048 /// The value of the change output. This is 0 for a changeless transaction or
6149 /// negative if there aren't enough funds.
6250 late final BigInt changeValue;
6351 /// The maximum size of the transaction after being fully signed
64- late final int signedSize;
65-
66- int _sizeGivenChange (int fixedSize, bool includeChange)
67- => fixedSize
68- + recipients.fold (0 , (acc, output) => acc + output.size)
69- + (includeChange ? Output .fromProgram (BigInt .zero, changeProgram).size : 0 )
70- + MeasureWriter .varIntSizeOfInt (
71- recipients.length + (includeChange ? 1 : 0 ),
72- ) as int ;
73-
74- BigInt _feeForSize (int size) {
75- final feeForSize = feePerKb * BigInt .from (size) ~ / BigInt .from (1000 );
76- return feeForSize.compareTo (minFee) > 0 ? feeForSize : minFee;
77- }
52+ late int signedSize;
7853
7954 /// Selects all the inputs from [selected] to send to the [recipients] outputs
8055 /// and provide change to the [changeProgram] . The [feePerKb] specifies the
8156 /// required fee in sats per KB with a minimum fee specified with
8257 /// [minFee] . The [minChange] is the minimum allowed change.
58+ ///
59+ /// Will throw [ArgumentError] if a [selected] input does not have a
60+ /// calculable size.
8361 CoinSelection ({
8462 this .version = Transaction .currentVersion,
8563 required Iterable <InputCandidate > selected,
@@ -97,63 +75,42 @@ class CoinSelection {
9775 }
9876
9977 // Get input and recipient values
100- inputValue = selected
101- .fold (BigInt .zero, (acc, candidate) => acc + candidate.value);
102- recipientValue = recipients
103- .fold (BigInt .zero, (acc, output) => acc + output.value);
104-
105- final isWitness = selected.any (
106- (candidate) => candidate.input is WitnessInput ,
107- );
108-
109- // Get unchanging size
110- final int fixedSize
111- // Version and locktime
112- = 8
113- // Add witness marker and flag
114- + (isWitness ? 2 : 0 )
115- // Fully signed inputs
116- + MeasureWriter .varIntSizeOfInt (selected.length)
117- + selected.fold (
118- 0 ,
119- (acc, candidate) {
120- final input = candidate.input;
121- final inputSize = input is TaprootInput && candidate.defaultSigHash
122- ? input.defaultSignedSize
123- : input.signedSize;
124- return acc + inputSize! ;
125- }
126- );
127-
128- // Determine size and fee with change
129- final sizeWithChange = _sizeGivenChange (fixedSize, true );
130- final feeWithChange = _feeForSize (sizeWithChange);
131- final includedChangeValue = inputValue - recipientValue - feeWithChange;
132-
133- // If change is under the required minimum, remove the change output
134- if (includedChangeValue.compareTo (minChange) < 0 ) {
135-
136- final changelessSize = _sizeGivenChange (fixedSize, false );
137- final feeForSize = _feeForSize (changelessSize);
138- final excess = inputValue - recipientValue - feeForSize;
139-
140- if (! excess.isNegative) {
141- // Exceeded without change. Fee is the input value minus the recipient
142- // value
143- signedSize = changelessSize;
144- fee = inputValue - recipientValue;
145- changeValue = BigInt .zero;
146- return ;
147- }
148- // Else haven't met requirement
149-
78+ inputValue = addBigInts (selected.map ((candidate) => candidate.value));
79+ recipientValue = addBigInts (recipients.map ((output) => output.value));
80+ final inputExcess = inputValue - recipientValue;
81+
82+ final inputs = selected.map ((candidate) => candidate.input).toList ();
83+ final isWitness = Transaction .inputsHaveWitness (inputs);
84+ final outputProgram = Output .fromProgram (BigInt .zero, changeProgram);
85+
86+ int getSize (bool withChange) => Transaction .calculateSignedSize (
87+ inputs: inputs,
88+ outputs: [...recipients, if (withChange) outputProgram ],
89+ isWitness: isWitness,
90+ )! ; // Assert null as all inputs will have signedSize as tested above
91+
92+ BigInt getFeeExcess (int size)
93+ => inputExcess - Transaction .calculateFee (size, feePerKb, minFee);
94+
95+ // Try to create change tranasction first
96+ final sizeWithChange = getSize (true );
97+ final change = getFeeExcess (sizeWithChange);
98+
99+ // Transaction with change is successful if the change is above the minimum
100+ if (change.compareTo (minChange) >= 0 ) {
101+ signedSize = sizeWithChange;
102+ changeValue = change;
103+ return ;
150104 }
151105
152- // Either haven't met requirement, or have met requirement with change so
153- // provide details of change-containing transaction
154- signedSize = sizeWithChange;
155- fee = feeWithChange;
156- changeValue = includedChangeValue;
106+ // Else target the transaction without the change output
107+ signedSize = getSize (false );
108+ final feeExcess = getFeeExcess (signedSize);
109+
110+ // Clamp the change value to no more than 0 as it is a changeless
111+ // transaction.
112+ // If the excess is negative, it is a shortfall.
113+ changeValue = feeExcess.isNegative ? feeExcess : BigInt .zero;
157114
158115 }
159116
@@ -203,9 +160,11 @@ class CoinSelection {
203160 /// in the order that they are given until the required amount has been
204161 /// reached. If there are not enough coins, all shall be selected and
205162 /// [enoughFunds] shall be false.
163+ ///
206164 /// If [randomise] is set to true, the order of inputs shall be randomised
207165 /// after being selected. This is useful for candidates that are not already
208166 /// randomised as it may avoid giving clues to the algorithm being used.
167+ ///
209168 /// The algorithm will only take upto 6800 candidates by default to avoid
210169 /// taking too long and due to size limitations. This can be changed with
211170 /// [maxCandidates] .
@@ -234,15 +193,29 @@ class CoinSelection {
234193 locktime: locktime,
235194 );
236195
196+ if (candidates.isEmpty) return trySelection ([]);
197+
237198 // Restrict number of candidates due to size limitation and for efficiency
238199 final list = candidates.take (maxCandidates).toList ();
239200
240- CoinSelection selection = trySelection ([]);
241- for (int i = 0 ; i < list.length; i++ ) {
242- selection = trySelection (list.take (i+ 1 ));
243- if (selection.enoughFunds) break ;
201+ // Use binary search to find the required amount
202+ CoinSelection search (int left, int right, CoinSelection ? cacheRight) {
203+
204+ if (left == right) {
205+ return cacheRight ?? trySelection (list.take (left));
206+ }
207+
208+ final middle = (left + right) ~ / 2 ;
209+ final middleSelection = trySelection (list.take (middle));
210+
211+ return middleSelection.enoughFunds
212+ ? search (left, middle, middleSelection)
213+ : search (middle+ 1 , right, cacheRight);
214+
244215 }
245216
217+ final selection = search (1 , list.length, null );
218+
246219 return randomise
247220 ? trySelection (selection.selected.toList ()..shuffle ())
248221 : selection;
@@ -303,17 +276,23 @@ class CoinSelection {
303276 /// value and fee, or [TransactionTooLarge] if the resulting signed
304277 /// transaction would be too large.
305278 Transaction get transaction {
279+
306280 if (! enoughFunds) throw InsufficientFunds ();
307281 if (tooLarge) throw TransactionTooLarge ();
282+
308283 return Transaction (
309284 version: version,
310285 inputs: selected.map ((candidate) => candidate.input),
311- outputs: changeless ? recipients : insertRandom (
312- recipients,
313- Output .fromProgram (changeValue, changeProgram),
314- ),
286+ outputs: changeless
287+ ? recipients
288+ : insertRandom (
289+ recipients,
290+ Output .fromProgram (changeValue, changeProgram),
291+ ),
315292 locktime: locktime,
293+ skipSizeCheck: true ,
316294 );
295+
317296 }
318297
319298 /// True when the input value covers the outputs and fee
@@ -324,5 +303,7 @@ class CoinSelection {
324303 bool get tooLarge => signedSize > Transaction .maxSize;
325304 /// True if a signable solution has been found
326305 bool get ready => enoughFunds && ! tooLarge;
306+ /// The fee to be paid by the transaction
307+ BigInt get fee => inputValue - recipientValue - changeValue;
327308
328309}
0 commit comments