lnwallet: add implementation of WebApiFeeSource for external APIs

This enables users to specify an external API for fee estimation.
The API is expected to return fees in the JSON format:
`{
	fee_by_block_target: {
	   a: x,
	   b: y,
	   ...
	   c: z
        }
 }`
 where a, b, c are block targets and x, y, z are fees in sat/kb.
 Note that a, b, c need not be contiguous.
This commit is contained in:
Valentine Wallace 2019-04-08 19:31:36 -07:00
parent 9b8549011c
commit 4944eb3e54
2 changed files with 101 additions and 2 deletions

@ -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)

@ -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")
}
}