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  }