diff --git a/lnwallet/fee_estimator.go b/lnwallet/fee_estimator.go index e075595f7..f8e762914 100644 --- a/lnwallet/fee_estimator.go +++ b/lnwallet/fee_estimator.go @@ -2,6 +2,7 @@ package lnwallet import ( "encoding/json" + "io" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/rpcclient" @@ -474,3 +475,46 @@ type WebAPIFeeSource interface { // specifics are left to the WebAPIFeeSource implementation. ParseResponse(r io.Reader) (map[uint32]uint32, error) } + +// SparseConfFeeSource is an implementation of the WebAPIFeeSource that utilizes +// a user-specified fee estimation API for Bitcoin. It expects the response +// to be in the JSON format: `fee_by_block_target: { ... }` where the value maps +// block targets to fee estimates (in sat per kilovbyte). +type SparseConfFeeSource struct { + // URL is the fee estimation API specified by the user. + URL string +} + +// GenQueryURL generates the full query URL. The value returned by this +// method should be able to be used directly as a path for an HTTP GET +// request. +// +// NOTE: Part of the WebAPIFeeSource interface. +func (s SparseConfFeeSource) GenQueryURL() string { + return s.URL +} + +// ParseResponse attempts to parse the body of the response generated by the +// above query URL. Typically this will be JSON, but the specifics are left to +// the WebAPIFeeSource implementation. +// +// NOTE: Part of the WebAPIFeeSource interface. +func (s SparseConfFeeSource) ParseResponse(r io.Reader) (map[uint32]uint32, error) { + type jsonResp struct { + FeeByBlockTarget map[uint32]uint32 `json:"fee_by_block_target"` + } + + resp := jsonResp{ + FeeByBlockTarget: make(map[uint32]uint32), + } + jsonReader := json.NewDecoder(r) + if err := jsonReader.Decode(&resp); err != nil { + return nil, err + } + + return resp.FeeByBlockTarget, nil +} + +// A compile-time assertion to ensure that SparseConfFeeSource implements the +// WebAPIFeeSource interface. +var _ WebAPIFeeSource = (*SparseConfFeeSource)(nil) diff --git a/lnwallet/fee_estimator_test.go b/lnwallet/fee_estimator_test.go index 77d60916c..d91df345d 100644 --- a/lnwallet/fee_estimator_test.go +++ b/lnwallet/fee_estimator_test.go @@ -1,6 +1,9 @@ package lnwallet_test import ( + "bytes" + "encoding/json" + "reflect" "testing" "github.com/btcsuite/btcutil" @@ -67,8 +70,8 @@ func TestFeeRateTypes(t *testing.T) { } } -// TestStaticFeeEstimator checks that the StaticFeeEstimator -// returns the expected fee rate. +// TestStaticFeeEstimator checks that the StaticFeeEstimator returns the +// expected fee rate. func TestStaticFeeEstimator(t *testing.T) { t.Parallel() @@ -89,3 +92,55 @@ func TestStaticFeeEstimator(t *testing.T) { 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 := lnwallet.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") + } +}