github.com/decred/dcrlnd@v0.7.6/lnwallet/chainfee/estimator_test.go (about) 1 package chainfee 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "io" 7 "net/http" 8 "reflect" 9 "strings" 10 "testing" 11 12 "github.com/davecgh/go-spew/spew" 13 "github.com/stretchr/testify/require" 14 ) 15 16 type mockSparseConfFeeSource struct { 17 url string 18 fees map[uint32]uint32 19 } 20 21 func (e mockSparseConfFeeSource) GenQueryURL() string { 22 return e.url 23 } 24 25 func (e mockSparseConfFeeSource) ParseResponse(r io.Reader) (map[uint32]uint32, error) { 26 return e.fees, nil 27 } 28 29 type emptyReadCloser struct{} 30 31 func (e emptyReadCloser) Read(b []byte) (int, error) { 32 return 0, nil 33 } 34 func (e emptyReadCloser) Close() error { 35 return nil 36 } 37 38 func emptyGetter(url string) (*http.Response, error) { 39 return &http.Response{ 40 Body: emptyReadCloser{}, 41 }, nil 42 } 43 44 // TestStaticFeeEstimator checks that the StaticFeeEstimator returns the 45 // expected fee rate. 46 func TestStaticFeeEstimator(t *testing.T) { 47 t.Parallel() 48 49 const feePerKb = FeePerKBFloor 50 51 feeEstimator := NewStaticEstimator(feePerKb, 0) 52 if err := feeEstimator.Start(); err != nil { 53 t.Fatalf("unable to start fee estimator: %v", err) 54 } 55 defer feeEstimator.Stop() 56 57 feeRate, err := feeEstimator.EstimateFeePerKB(6) 58 if err != nil { 59 t.Fatalf("unable to get fee rate: %v", err) 60 } 61 62 if feeRate != feePerKb { 63 t.Fatalf("expected fee rate %v, got %v", feePerKb, feeRate) 64 } 65 } 66 67 // TestSparseConfFeeSource checks that SparseConfFeeSource generates URLs and 68 // parses API responses as expected. 69 func TestSparseConfFeeSource(t *testing.T) { 70 t.Parallel() 71 72 // Test that GenQueryURL returns the URL as is. 73 url := "test" 74 feeSource := SparseConfFeeSource{URL: url} 75 queryURL := feeSource.GenQueryURL() 76 if queryURL != url { 77 t.Fatalf("expected query URL of %v, got %v", url, queryURL) 78 } 79 80 // Test parsing a properly formatted JSON API response. 81 // First, create the response as a bytes.Reader. 82 testFees := map[uint32]uint32{ 83 1: 12345, 84 2: 42, 85 3: 54321, 86 } 87 testJSON := map[string]map[uint32]uint32{"fee_by_block_target": testFees} 88 jsonResp, err := json.Marshal(testJSON) 89 if err != nil { 90 t.Fatalf("unable to marshal JSON API response: %v", err) 91 } 92 reader := bytes.NewReader(jsonResp) 93 94 // Finally, ensure the expected map is returned without error. 95 fees, err := feeSource.ParseResponse(reader) 96 if err != nil { 97 t.Fatalf("unable to parse API response: %v", err) 98 } 99 if !reflect.DeepEqual(fees, testFees) { 100 t.Fatalf("expected %v, got %v", testFees, fees) 101 } 102 103 // Test parsing an improperly formatted JSON API response. 104 badFees := map[string]uint32{"hi": 12345, "hello": 42, "satoshi": 54321} 105 badJSON := map[string]map[string]uint32{"fee_by_block_target": badFees} 106 jsonResp, err = json.Marshal(badJSON) 107 if err != nil { 108 t.Fatalf("unable to marshal JSON API response: %v", err) 109 } 110 reader = bytes.NewReader(jsonResp) 111 112 // Finally, ensure the improperly formatted fees error. 113 _, err = feeSource.ParseResponse(reader) 114 if err == nil { 115 t.Fatalf("expected ParseResponse to fail") 116 } 117 } 118 119 // TestWebAPIFeeEstimator checks that the WebAPIFeeEstimator returns fee rates 120 // as expected. 121 func TestWebAPIFeeEstimator(t *testing.T) { 122 t.Parallel() 123 feeFloor := uint32(FeePerKBFloor) 124 testFeeRate := feeFloor * 100 125 126 testCases := []struct { 127 name string 128 target uint32 129 apiEst uint32 130 est uint32 131 err string 132 }{ 133 { 134 name: "target_below_min", 135 target: 0, 136 apiEst: 0, 137 est: 0, 138 err: "too low, minimum", 139 }, 140 { 141 name: "target_w_too-low_fee", 142 target: 100, 143 apiEst: 42, 144 est: feeFloor, 145 err: "", 146 }, 147 { 148 name: "API-omitted_target", 149 target: 2, 150 apiEst: 0, 151 est: testFeeRate, 152 err: "", 153 }, 154 { 155 name: "valid_target", 156 target: 20, 157 apiEst: testFeeRate, 158 est: testFeeRate, 159 err: "", 160 }, 161 { 162 name: "valid_target_extrapolated_fee", 163 target: 25, 164 apiEst: 0, 165 est: testFeeRate, 166 err: "", 167 }, 168 } 169 170 // Construct mock fee source for the Estimator to pull fees from. 171 testFees := make(map[uint32]uint32) 172 for _, tc := range testCases { 173 if tc.apiEst != 0 { 174 testFees[tc.target] = tc.apiEst 175 } 176 } 177 178 spew.Dump(testFees) 179 180 feeSource := mockSparseConfFeeSource{ 181 url: "https://www.github.com", 182 fees: testFees, 183 } 184 185 estimator := NewWebAPIEstimator(feeSource, false) 186 estimator.netGetter = emptyGetter 187 188 // Test that requesting a fee when no fees have been cached fails. 189 feeRate, err := estimator.EstimateFeePerKB(5) 190 require.NoErrorf(t, err, "expected no error") 191 require.Equalf(t, FeePerKBFloor, feeRate, "expected fee rate floor "+ 192 "returned when no cached fee rate found") 193 194 if err := estimator.Start(); err != nil { 195 t.Fatalf("unable to start fee estimator, got: %v", err) 196 } 197 defer estimator.Stop() 198 199 for _, tc := range testCases { 200 tc := tc 201 t.Run(tc.name, func(t *testing.T) { 202 est, err := estimator.EstimateFeePerKB(tc.target) 203 if tc.err != "" { 204 if err == nil || 205 !strings.Contains(err.Error(), tc.err) { 206 207 t.Fatalf("expected fee estimation to "+ 208 "fail, instead got: %v", err) 209 } 210 } else { 211 exp := AtomPerKByte(tc.est) 212 if err != nil { 213 t.Fatalf("unable to estimate fee for "+ 214 "%v block target, got: %v", 215 tc.target, err) 216 } 217 if est != exp { 218 t.Fatalf("expected fee estimate of "+ 219 "%v, got %v", exp, est) 220 } 221 } 222 }) 223 } 224 } 225 226 // TestGetCachedFee checks that the fee caching logic works as expected. 227 func TestGetCachedFee(t *testing.T) { 228 target := uint32(2) 229 fee := uint32(100) 230 231 // Create a dummy estimator without WebAPIFeeSource. 232 estimator := NewWebAPIEstimator(nil, false) 233 234 // When the cache is empty, an error should be returned. 235 cachedFee, err := estimator.getCachedFee(target) 236 require.Zero(t, cachedFee) 237 require.ErrorIs(t, err, errEmptyCache) 238 239 // Store a fee rate inside the cache. 240 estimator.feeByBlockTarget[target] = fee 241 242 testCases := []struct { 243 name string 244 confTarget uint32 245 expectedFee uint32 246 expectErr error 247 }{ 248 { 249 // When the target is cached, return it. 250 name: "return cached fee", 251 confTarget: target, 252 expectedFee: fee, 253 expectErr: nil, 254 }, 255 { 256 // When the target is not cached, return the next 257 // lowest target that's cached. 258 name: "return next cached fee", 259 confTarget: target + 1, 260 expectedFee: fee, 261 expectErr: nil, 262 }, 263 { 264 // When the target is not cached, and the next lowest 265 // target is not cached, return the nearest fee rate. 266 name: "return highest cached fee", 267 confTarget: target - 1, 268 expectedFee: fee, 269 expectErr: nil, 270 }, 271 } 272 273 for _, tc := range testCases { 274 tc := tc 275 276 t.Run(tc.name, func(t *testing.T) { 277 cachedFee, err := estimator.getCachedFee(tc.confTarget) 278 279 require.Equal(t, tc.expectedFee, cachedFee) 280 require.ErrorIs(t, err, tc.expectErr) 281 }) 282 } 283 284 }