1abc5f8b00
This commit changes the minBlockTarget used by the WebAPIEstimator to be 1, inline with the bitcoind's accepted min conf target.
236 lines
6.3 KiB
Go
236 lines
6.3 KiB
Go
package chainfee
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcutil"
|
|
)
|
|
|
|
type mockSparseConfFeeSource struct {
|
|
url string
|
|
fees map[uint32]uint32
|
|
}
|
|
|
|
func (e mockSparseConfFeeSource) GenQueryURL() string {
|
|
return e.url
|
|
}
|
|
|
|
func (e mockSparseConfFeeSource) ParseResponse(r io.Reader) (map[uint32]uint32, error) {
|
|
return e.fees, nil
|
|
}
|
|
|
|
// TestFeeRateTypes checks that converting fee rates between the
|
|
// different types that represent fee rates and calculating fees
|
|
// work as expected.
|
|
func TestFeeRateTypes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// We'll be calculating the transaction fees for the given measurements
|
|
// using different fee rates and expecting them to match.
|
|
const vsize = 300
|
|
const weight = vsize * 4
|
|
|
|
// Test the conversion from sat/kw to sat/kb.
|
|
for feePerKw := SatPerKWeight(250); feePerKw < 10000; feePerKw += 50 {
|
|
feePerKB := feePerKw.FeePerKVByte()
|
|
if feePerKB != SatPerKVByte(feePerKw*4) {
|
|
t.Fatalf("expected %d sat/kb, got %d sat/kb when "+
|
|
"converting from %d sat/kw", feePerKw*4,
|
|
feePerKB, feePerKw)
|
|
}
|
|
|
|
// The resulting transaction fee should be the same when using
|
|
// both rates.
|
|
expectedFee := btcutil.Amount(feePerKw * weight / 1000)
|
|
fee1 := feePerKw.FeeForWeight(weight)
|
|
if fee1 != expectedFee {
|
|
t.Fatalf("expected fee of %d sats, got %d sats",
|
|
expectedFee, fee1)
|
|
}
|
|
fee2 := feePerKB.FeeForVSize(vsize)
|
|
if fee2 != expectedFee {
|
|
t.Fatalf("expected fee of %d sats, got %d sats",
|
|
expectedFee, fee2)
|
|
}
|
|
}
|
|
|
|
// Test the conversion from sat/kb to sat/kw.
|
|
for feePerKB := SatPerKVByte(1000); feePerKB < 40000; feePerKB += 1000 {
|
|
feePerKw := feePerKB.FeePerKWeight()
|
|
if feePerKw != SatPerKWeight(feePerKB/4) {
|
|
t.Fatalf("expected %d sat/kw, got %d sat/kw when "+
|
|
"converting from %d sat/kb", feePerKB/4,
|
|
feePerKw, feePerKB)
|
|
}
|
|
|
|
// The resulting transaction fee should be the same when using
|
|
// both rates.
|
|
expectedFee := btcutil.Amount(feePerKB * vsize / 1000)
|
|
fee1 := feePerKB.FeeForVSize(vsize)
|
|
if fee1 != expectedFee {
|
|
t.Fatalf("expected fee of %d sats, got %d sats",
|
|
expectedFee, fee1)
|
|
}
|
|
fee2 := feePerKw.FeeForWeight(weight)
|
|
if fee2 != expectedFee {
|
|
t.Fatalf("expected fee of %d sats, got %d sats",
|
|
expectedFee, fee2)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestStaticFeeEstimator checks that the StaticFeeEstimator returns the
|
|
// expected fee rate.
|
|
func TestStaticFeeEstimator(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const feePerKw = FeePerKwFloor
|
|
|
|
feeEstimator := NewStaticEstimator(feePerKw, 0)
|
|
if err := feeEstimator.Start(); err != nil {
|
|
t.Fatalf("unable to start fee estimator: %v", err)
|
|
}
|
|
defer feeEstimator.Stop()
|
|
|
|
feeRate, err := feeEstimator.EstimateFeePerKW(6)
|
|
if err != nil {
|
|
t.Fatalf("unable to get fee rate: %v", err)
|
|
}
|
|
|
|
if feeRate != feePerKw {
|
|
t.Fatalf("expected fee rate %v, got %v", feePerKw, feeRate)
|
|
}
|
|
}
|
|
|
|
// TestSparseConfFeeSource checks that SparseConfFeeSource generates URLs and
|
|
// parses API responses as expected.
|
|
func TestSparseConfFeeSource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test that GenQueryURL returns the URL as is.
|
|
url := "test"
|
|
feeSource := SparseConfFeeSource{URL: url}
|
|
queryURL := feeSource.GenQueryURL()
|
|
if queryURL != url {
|
|
t.Fatalf("expected query URL of %v, got %v", url, queryURL)
|
|
}
|
|
|
|
// Test parsing a properly formatted JSON API response.
|
|
// First, create the response as a bytes.Reader.
|
|
testFees := map[uint32]uint32{
|
|
1: 12345,
|
|
2: 42,
|
|
3: 54321,
|
|
}
|
|
testJSON := map[string]map[uint32]uint32{"fee_by_block_target": testFees}
|
|
jsonResp, err := json.Marshal(testJSON)
|
|
if err != nil {
|
|
t.Fatalf("unable to marshal JSON API response: %v", err)
|
|
}
|
|
reader := bytes.NewReader(jsonResp)
|
|
|
|
// Finally, ensure the expected map is returned without error.
|
|
fees, err := feeSource.ParseResponse(reader)
|
|
if err != nil {
|
|
t.Fatalf("unable to parse API response: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(fees, testFees) {
|
|
t.Fatalf("expected %v, got %v", testFees, fees)
|
|
}
|
|
|
|
// Test parsing an improperly formatted JSON API response.
|
|
badFees := map[string]uint32{"hi": 12345, "hello": 42, "satoshi": 54321}
|
|
badJSON := map[string]map[string]uint32{"fee_by_block_target": badFees}
|
|
jsonResp, err = json.Marshal(badJSON)
|
|
if err != nil {
|
|
t.Fatalf("unable to marshal JSON API response: %v", err)
|
|
}
|
|
reader = bytes.NewReader(jsonResp)
|
|
|
|
// Finally, ensure the improperly formatted fees error.
|
|
_, err = feeSource.ParseResponse(reader)
|
|
if err == nil {
|
|
t.Fatalf("expected ParseResponse to fail")
|
|
}
|
|
}
|
|
|
|
// TestWebAPIFeeEstimator checks that the WebAPIFeeEstimator returns fee rates
|
|
// as expected.
|
|
func TestWebAPIFeeEstimator(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
feeFloor := uint32(FeePerKwFloor.FeePerKVByte())
|
|
testCases := []struct {
|
|
name string
|
|
target uint32
|
|
apiEst uint32
|
|
est uint32
|
|
err string
|
|
}{
|
|
{"target_below_min", 0, 12345, 12345, "too low, minimum"},
|
|
{"target_w_too-low_fee", 10, 42, feeFloor, ""},
|
|
{"API-omitted_target", 2, 0, 0, "web API does not include"},
|
|
{"valid_target", 20, 54321, 54321, ""},
|
|
{"valid_target_extrapolated_fee", 25, 0, 54321, ""},
|
|
}
|
|
|
|
// Construct mock fee source for the Estimator to pull fees from.
|
|
testFees := make(map[uint32]uint32)
|
|
for _, tc := range testCases {
|
|
if tc.apiEst != 0 {
|
|
testFees[tc.target] = tc.apiEst
|
|
}
|
|
}
|
|
|
|
feeSource := mockSparseConfFeeSource{
|
|
url: "https://www.github.com",
|
|
fees: testFees,
|
|
}
|
|
|
|
estimator := NewWebAPIEstimator(feeSource, false)
|
|
|
|
// Test that requesting a fee when no fees have been cached fails.
|
|
_, err := estimator.EstimateFeePerKW(5)
|
|
if err == nil ||
|
|
!strings.Contains(err.Error(), "web API does not include") {
|
|
|
|
t.Fatalf("expected fee estimation to fail, instead got: %v", err)
|
|
}
|
|
|
|
if err := estimator.Start(); err != nil {
|
|
t.Fatalf("unable to start fee estimator, got: %v", err)
|
|
}
|
|
defer estimator.Stop()
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
est, err := estimator.EstimateFeePerKW(tc.target)
|
|
if tc.err != "" {
|
|
if err == nil ||
|
|
!strings.Contains(err.Error(), tc.err) {
|
|
|
|
t.Fatalf("expected fee estimation to "+
|
|
"fail, instead got: %v", err)
|
|
}
|
|
} else {
|
|
exp := SatPerKVByte(tc.est).FeePerKWeight()
|
|
if err != nil {
|
|
t.Fatalf("unable to estimate fee for "+
|
|
"%v block target, got: %v",
|
|
tc.target, err)
|
|
}
|
|
if est != exp {
|
|
t.Fatalf("expected fee estimate of "+
|
|
"%v, got %v", exp, est)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|