dev
dart 88 lines 3.29 KB
Raw
1 import 'dart:math';
2
3 int effectiveValue(int value, int inputCost) => value - inputCost;
4
5 class SelectedCoins {
6 final List<int> indices;
7 final bool hasChange;
8 const SelectedCoins(this.indices, this.hasChange);
9 }
10
11 SelectedCoins? branchAndBound(List<int> effValues, int target, int costOfChange,
12 {int maxTries = 100000}) {
13 final n = effValues.length;
14 final order = List<int>.generate(n, (i) => i)
15 ..sort((a, b) => effValues[b].compareTo(effValues[a]));
16 final sorted = [for (final i in order) effValues[i]];
17 final suffix = List<int>.filled(n + 1, 0);
18 for (var i = n - 1; i >= 0; i--) suffix[i] = suffix[i + 1] + sorted[i];
19 final upper = target + costOfChange;
20 List<int>? best;
21 final picked = <int>[];
22 var tries = 0;
23 void dfs(int i, int sum) {
24 if (best != null || tries++ >= maxTries) return;
25 if (sum > upper) return;
26 if (sum >= target) { best = List.of(picked); return; }
27 if (i >= n || sum + suffix[i] < target) return;
28 picked.add(order[i]); dfs(i + 1, sum + sorted[i]); picked.removeLast();
29 dfs(i + 1, sum);
30 }
31 dfs(0, 0);
32 return best == null ? null : SelectedCoins(best!, false);
33 }
34
35 SelectedCoins? singleRandomDraw(List<int> effValues, int target, int minChange, Random rng) {
36 final order = List<int>.generate(effValues.length, (i) => i)..shuffle(rng);
37 final picked = <int>[]; var sum = 0;
38 for (final i in order) {
39 picked.add(i); sum += effValues[i];
40 if (sum >= target + minChange) return SelectedCoins(picked, true);
41 }
42 return null;
43 }
44
45 /// Branch-and-bound over effective values (value minus the cost of spending the
46 /// input at the current fee rate). [inputCosts] is parallel to [values] so each
47 /// input pays for its own script type's size. A match means the excess over
48 /// [target] stays within [window], so the remainder can be absorbed into the fee
49 /// instead of creating a change output. Returns null when no such subset exists.
50 SelectedCoins? changelessMatch({
51 required List<int> values,
52 required int target,
53 required List<int> inputCosts,
54 required int window,
55 int maxTries = 100000,
56 }) {
57 assert(values.length == inputCosts.length);
58 final keep = <int>[];
59 final eff = <int>[];
60 for (var i = 0; i < values.length; i++) {
61 final e = effectiveValue(values[i], inputCosts[i]);
62 if (e > 0) {
63 keep.add(i);
64 eff.add(e);
65 }
66 }
67 final match = branchAndBound(eff, target, window, maxTries: maxTries);
68 if (match == null) return null;
69 return SelectedCoins([for (final i in match.indices) keep[i]], false);
70 }
71
72 class InsufficientFundsException implements Exception { const InsufficientFundsException(); }
73
74 SelectedCoins selectCoins({required List<int> values, required int target,
75 required int inputCost, required int costOfChange, required int minChange, Random? rng}) {
76 final keep = <int>[]; final eff = <int>[];
77 for (var i = 0; i < values.length; i++) {
78 final e = effectiveValue(values[i], inputCost);
79 if (e > 0) { keep.add(i); eff.add(e); }
80 }
81 SelectedCoins? map(SelectedCoins? s) =>
82 s == null ? null : SelectedCoins([for (final i in s.indices) keep[i]], s.hasChange);
83 final bnb = map(branchAndBound(eff, target, costOfChange));
84 if (bnb != null) return bnb;
85 final srd = map(singleRandomDraw(eff, target, minChange, rng ?? Random.secure()));
86 if (srd != null) return srd;
87 throw const InsufficientFundsException();
88 }