Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add mempool.space alternative blockchain fees provider
- Loading branch information
1 parent
0ed95dc
commit 5f47f3c
Showing
6 changed files
with
329 additions
and
85 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package btc | ||
|
||
import ( | ||
"fmt" | ||
"math/big" | ||
"sync" | ||
"time" | ||
|
||
"github.com/golang/glog" | ||
"github.com/juju/errors" | ||
"github.com/trezor/blockbook/bchain" | ||
) | ||
|
||
type alternativeFeeProviderFee struct { | ||
blocks int | ||
feePerKB int | ||
} | ||
|
||
type alternativeFeeProvider struct { | ||
fees []alternativeFeeProviderFee | ||
lastSync time.Time | ||
chain bchain.BlockChain | ||
mux sync.Mutex | ||
} | ||
|
||
type alternativeFeeProviderInterface interface { | ||
compareToDefault() | ||
estimateFee(blocks int) (big.Int, error) | ||
} | ||
|
||
func (p *alternativeFeeProvider) compareToDefault() { | ||
output := "" | ||
for _, fee := range p.fees { | ||
conservative, err := p.chain.(*BitcoinRPC).blockchainEstimateSmartFee(fee.blocks, true) | ||
if err != nil { | ||
glog.Error(err) | ||
return | ||
} | ||
economical, err := p.chain.(*BitcoinRPC).blockchainEstimateSmartFee(fee.blocks, false) | ||
if err != nil { | ||
glog.Error(err) | ||
return | ||
} | ||
output += fmt.Sprintf("Blocks %d: alternative %d, conservative %s, economical %s\n", fee.blocks, fee.feePerKB, conservative.String(), economical.String()) | ||
} | ||
glog.Info("alternativeFeeProviderCompareToDefault\n", output) | ||
} | ||
|
||
func (p *alternativeFeeProvider) estimateFee(blocks int) (big.Int, error) { | ||
var r big.Int | ||
p.mux.Lock() | ||
defer p.mux.Unlock() | ||
if len(p.fees) == 0 { | ||
return r, errors.New("alternativeFeeProvider: no fees") | ||
} | ||
if p.lastSync.Before(time.Now().Add(time.Duration(-10) * time.Minute)) { | ||
return r, errors.Errorf("alternativeFeeProvider: Missing recent value, last sync at %v", p.lastSync) | ||
} | ||
for i := range p.fees { | ||
if p.fees[i].blocks >= blocks { | ||
r = *big.NewInt(int64(p.fees[i].feePerKB)) | ||
return r, nil | ||
} | ||
} | ||
// use the last value as fallback | ||
r = *big.NewInt(int64(p.fees[len(p.fees)-1].feePerKB)) | ||
return r, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
package btc | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"net/http" | ||
"strconv" | ||
"time" | ||
|
||
"github.com/golang/glog" | ||
"github.com/juju/errors" | ||
"github.com/trezor/blockbook/bchain" | ||
) | ||
|
||
// https://mempool.space/api/v1/fees/recommended returns | ||
// {"fastestFee":41,"halfHourFee":39,"hourFee":36,"economyFee":36,"minimumFee":20} | ||
|
||
type mempoolSpaceFeeResult struct { | ||
FastestFee int `json:"fastestFee"` | ||
HalfHourFee int `json:"halfHourFee"` | ||
HourFee int `json:"hourFee"` | ||
EconomyFee int `json:"economyFee"` | ||
MinimumFee int `json:"minimumFee"` | ||
} | ||
|
||
type mempoolSpaceFeeParams struct { | ||
URL string `json:"url"` | ||
PeriodSeconds int `periodSeconds:"url"` | ||
} | ||
|
||
type mempoolSpaceFeeProvider struct { | ||
*alternativeFeeProvider | ||
params mempoolSpaceFeeParams | ||
} | ||
|
||
// NewMempoolSpaceFee initializes https://mempool.space provider | ||
func NewMempoolSpaceFee(chain bchain.BlockChain, params string) (alternativeFeeProviderInterface, error) { | ||
p := &mempoolSpaceFeeProvider{alternativeFeeProvider: &alternativeFeeProvider{}} | ||
err := json.Unmarshal([]byte(params), &p.params) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if p.params.URL == "" || p.params.PeriodSeconds == 0 { | ||
return nil, errors.New("NewWhatTheFee: Missing parameters") | ||
} | ||
p.chain = chain | ||
go p.mempoolSpaceFeeDownloader() | ||
return p, nil | ||
} | ||
|
||
func (p *mempoolSpaceFeeProvider) mempoolSpaceFeeDownloader() { | ||
period := time.Duration(p.params.PeriodSeconds) * time.Second | ||
timer := time.NewTimer(period) | ||
counter := 0 | ||
for { | ||
var data mempoolSpaceFeeResult | ||
err := p.mempoolSpaceFeeGetData(&data) | ||
if err != nil { | ||
glog.Error("mempoolSpaceFeeGetData ", err) | ||
} else { | ||
if p.mempoolSpaceFeeProcessData(&data) { | ||
if counter%60 == 0 { | ||
p.compareToDefault() | ||
} | ||
counter++ | ||
} | ||
} | ||
<-timer.C | ||
timer.Reset(period) | ||
} | ||
} | ||
|
||
func (p *mempoolSpaceFeeProvider) mempoolSpaceFeeProcessData(data *mempoolSpaceFeeResult) bool { | ||
if data.MinimumFee == 0 || data.EconomyFee == 0 || data.HourFee == 0 || data.HalfHourFee == 0 || data.FastestFee == 0 { | ||
glog.Errorf("mempoolSpaceFeeProcessData: invalid data %+v", data) | ||
return false | ||
} | ||
p.mux.Lock() | ||
defer p.mux.Unlock() | ||
p.fees = make([]alternativeFeeProviderFee, 5) | ||
// map mempoool.space fees to blocks | ||
|
||
// FastestFee is for 1 block | ||
p.fees[0] = alternativeFeeProviderFee{ | ||
blocks: 1, | ||
feePerKB: data.FastestFee * 1000, | ||
} | ||
|
||
// HalfHourFee is for 2-5 blocks | ||
p.fees[1] = alternativeFeeProviderFee{ | ||
blocks: 5, | ||
feePerKB: data.HalfHourFee * 1000, | ||
} | ||
|
||
// HourFee is for 6-18 blocks | ||
p.fees[2] = alternativeFeeProviderFee{ | ||
blocks: 18, | ||
feePerKB: data.HourFee * 1000, | ||
} | ||
|
||
// EconomyFee is for 19-100 blocks | ||
p.fees[3] = alternativeFeeProviderFee{ | ||
blocks: 100, | ||
feePerKB: data.EconomyFee * 1000, | ||
} | ||
|
||
// MinimumFee is for over 100 blocks | ||
p.fees[4] = alternativeFeeProviderFee{ | ||
blocks: 500, | ||
feePerKB: data.MinimumFee * 1000, | ||
} | ||
|
||
p.lastSync = time.Now() | ||
// glog.Infof("mempoolSpaceFees: %+v", p.fees) | ||
return true | ||
} | ||
|
||
func (p *mempoolSpaceFeeProvider) mempoolSpaceFeeGetData(res interface{}) error { | ||
var httpData []byte | ||
httpReq, err := http.NewRequest("GET", p.params.URL, bytes.NewBuffer(httpData)) | ||
if err != nil { | ||
return err | ||
} | ||
httpRes, err := http.DefaultClient.Do(httpReq) | ||
if httpRes != nil { | ||
defer httpRes.Body.Close() | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
if httpRes.StatusCode != http.StatusOK { | ||
return errors.New(p.params.URL + " returned status " + strconv.Itoa(httpRes.StatusCode)) | ||
} | ||
return safeDecodeResponse(httpRes.Body, &res) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package btc | ||
|
||
import ( | ||
"math/big" | ||
"strconv" | ||
"testing" | ||
) | ||
|
||
func Test_mempoolSpaceFeeProvider(t *testing.T) { | ||
m := &mempoolSpaceFeeProvider{alternativeFeeProvider: &alternativeFeeProvider{}} | ||
m.mempoolSpaceFeeProcessData(&mempoolSpaceFeeResult{ | ||
MinimumFee: 10, | ||
EconomyFee: 20, | ||
HourFee: 30, | ||
HalfHourFee: 40, | ||
FastestFee: 50, | ||
}) | ||
|
||
tests := []struct { | ||
blocks int | ||
want big.Int | ||
}{ | ||
{0, *big.NewInt(50000)}, | ||
{1, *big.NewInt(50000)}, | ||
{2, *big.NewInt(40000)}, | ||
{5, *big.NewInt(40000)}, | ||
{6, *big.NewInt(30000)}, | ||
{10, *big.NewInt(30000)}, | ||
{18, *big.NewInt(30000)}, | ||
{19, *big.NewInt(20000)}, | ||
{100, *big.NewInt(20000)}, | ||
{101, *big.NewInt(10000)}, | ||
{500, *big.NewInt(10000)}, | ||
{5000000, *big.NewInt(10000)}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(strconv.Itoa(tt.blocks), func(t *testing.T) { | ||
got, err := m.estimateFee(tt.blocks) | ||
if err != nil { | ||
t.Error("estimateFee returned error ", err) | ||
} | ||
if got.Cmp(&tt.want) != 0 { | ||
t.Errorf("estimateFee(%d) = %v, want %v", tt.blocks, got, tt.want) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.