dev
dart 100 lines 4.81 KB
Raw
1 import 'dart:math';
2 import "package:cw_bitcoin/coin_selection.dart";
3 import "package:flutter_test/flutter_test.dart";
4 void main() {
5 test('effectiveValue', () {
6 expect(effectiveValue(10000, 136), 9864);
7 expect(effectiveValue(100, 136), -36);
8 });
9 test('BnB finds changeless match in window', () {
10 final r = branchAndBound([200,100,90,80], 300, 50);
11 expect(r, isNotNull); expect(r!.hasChange, isFalse);
12 final vals=[200,100,90,80]; final sum=r.indices.map((i)=>vals[i]).reduce((a,b)=>a+b);
13 expect(sum>=300 && sum<=350, isTrue);
14 });
15 test('BnB null when no subset in window', () {
16 expect(branchAndBound([1000,900], 300, 5), isNull);
17 });
18 test('SRD with change', () {
19 final r = singleRandomDraw([500,500,500,500], 700, 50, Random(1));
20 expect(r, isNotNull); expect(r!.hasChange, isTrue);
21 });
22 test('SRD randomizes across seeds', () {
23 final a = (singleRandomDraw([100,101,102,103,104,105],150,10,Random(1))!.indices..sort());
24 final b = (singleRandomDraw([100,101,102,103,104,105],150,10,Random(9))!.indices..sort());
25 expect(a, isNot(equals(b)));
26 });
27 test('SRD null on insufficient funds', () {
28 expect(singleRandomDraw([100,100], 500, 10, Random(1)), isNull);
29 });
30 test('selectCoins prefers changeless BnB, else SRD', () {
31 expect(selectCoins(values:[200,100,90],target:300,inputCost:0,costOfChange:20,minChange:10).hasChange, isFalse);
32 expect(selectCoins(values:[500,500,500],target:700,inputCost:0,costOfChange:5,minChange:10,rng:Random(1)).hasChange, isTrue);
33 });
34 test('selectCoins drops non-positive effective values', () {
35 // coin of value 50 with inputCost 136 -> effective -86 -> dropped; only the 1000 usable
36 final r = selectCoins(values:[50,1000],target:500,inputCost:136,costOfChange:5,minChange:10,rng:Random(1));
37 expect(r.indices, equals([1]));
38 });
39 test('changelessMatch finds a set whose effective sum lands in the window', () {
40 // inputCost 100: values [50, 400, 300, 200] -> eff [dropped, 300, 200, 100]
41 // target 500, window 46: eff {300, 200} = 500, exact
42 final r = changelessMatch(
43 values: [50, 400, 300, 200], target: 500, inputCosts: [100, 100, 100, 100], window: 46);
44 expect(r, isNotNull);
45 expect(r!.hasChange, isFalse);
46 final effSum = r.indices.map((i) => [50, 400, 300, 200][i] - 100).reduce((a, b) => a + b);
47 expect(effSum >= 500 && effSum <= 546, isTrue);
48 expect(r.indices.contains(0), isFalse); // negative-eff coin never selected
49 });
50 test('changelessMatch returns null when no subset lands in the window', () {
51 expect(
52 changelessMatch(values: [10000, 9000], target: 500, inputCosts: [100, 100], window: 46),
53 isNull);
54 });
55 test('selectCoins throws when insufficient', () {
56 expect(() => selectCoins(values:[100,100],target:500,inputCost:0,costOfChange:5,minChange:10),
57 throwsA(isA<InsufficientFundsException>()));
58 });
59 test('changeless pipeline: leftover absorbed into fee is non-negative and below dust', () {
60 // Mirrors the _createUTXOS / estimateTxForAmount arithmetic with the wallet's
61 // 68*inputs + 34*outputs + 10 vBytes model, at 10 sat/vB with 1 recipient output.
62 const feeRate = 10;
63 const amount = 50000;
64 final values = [60700, 30000, 21800, 9000, 5000];
65 const target = amount + (34 * 1 + 10) * feeRate;
66 final r = changelessMatch(
67 values: values,
68 target: target,
69 inputCosts: List.filled(values.length, 68 * feeRate),
70 window: 546);
71 expect(r, isNotNull);
72 final inAmount = r!.indices.map((i) => values[i]).reduce((a, b) => a + b);
73 final feeNoChange = (68 * r.indices.length + 34 * 1 + 10) * feeRate;
74 final leftover = inAmount - amount - feeNoChange;
75 expect(leftover >= 0, isTrue); // the caller never recurses for more inputs
76 expect(leftover <= 546, isTrue); // fee overpay is bounded by the dust limit
77 });
78 test('BnB terminates and returns null on large pools with no possible match', () {
79 // 300 even effective values, odd target, zero window: no subset can ever match,
80 // so the search must stop at maxTries instead of exploring 2^300 branches.
81 final values = List<int>.generate(300, (i) => 1000000 + i * 2);
82 final r = changelessMatch(
83 values: values, target: 1500001, inputCosts: List.filled(300, 10), window: 0);
84 expect(r, isNull);
85 });
86 test('changelessMatch charges each input its own script-type cost', () {
87 // Legacy input (148 vB) vs segwit input (68 vB) at 10 sat/vB: same value, but
88 // the legacy coin's effective value is 800 lower. Target only reachable when
89 // the cheaper segwit coin is chosen: eff segwit = 10000-680 = 9320.
90 const feeRate = 10;
91 final r = changelessMatch(
92 values: [10000, 10000],
93 target: 9320,
94 inputCosts: [148 * feeRate, 68 * feeRate],
95 window: 0,
96 );
97 expect(r, isNotNull);
98 expect(r!.indices, equals([1]));
99 });
100 }