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:
parent
9b8549011c
commit
4944eb3e54
@ -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")
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user