diff --git a/bchain/coins/btc/bitcoinrpc.go b/bchain/coins/btc/bitcoinrpc.go index ffda6650..1f36e556 100644 --- a/bchain/coins/btc/bitcoinrpc.go +++ b/bchain/coins/btc/bitcoinrpc.go @@ -36,25 +36,27 @@ type BitcoinRPC struct { // Configuration represents json config file type Configuration struct { - CoinName string `json:"coin_name"` - CoinShortcut string `json:"coin_shortcut"` - RPCURL string `json:"rpc_url"` - RPCUser string `json:"rpc_user"` - RPCPass string `json:"rpc_pass"` - RPCTimeout int `json:"rpc_timeout"` - Parse bool `json:"parse"` - MessageQueueBinding string `json:"message_queue_binding"` - Subversion string `json:"subversion"` - BlockAddressesToKeep int `json:"block_addresses_to_keep"` - MempoolWorkers int `json:"mempool_workers"` - MempoolSubWorkers int `json:"mempool_sub_workers"` - AddressFormat string `json:"address_format"` - SupportsEstimateFee bool `json:"supports_estimate_fee"` - SupportsEstimateSmartFee bool `json:"supports_estimate_smart_fee"` - XPubMagic uint32 `json:"xpub_magic,omitempty"` - XPubMagicSegwitP2sh uint32 `json:"xpub_magic_segwit_p2sh,omitempty"` - XPubMagicSegwitNative uint32 `json:"xpub_magic_segwit_native,omitempty"` - Slip44 uint32 `json:"slip44,omitempty"` + CoinName string `json:"coin_name"` + CoinShortcut string `json:"coin_shortcut"` + RPCURL string `json:"rpc_url"` + RPCUser string `json:"rpc_user"` + RPCPass string `json:"rpc_pass"` + RPCTimeout int `json:"rpc_timeout"` + Parse bool `json:"parse"` + MessageQueueBinding string `json:"message_queue_binding"` + Subversion string `json:"subversion"` + BlockAddressesToKeep int `json:"block_addresses_to_keep"` + MempoolWorkers int `json:"mempool_workers"` + MempoolSubWorkers int `json:"mempool_sub_workers"` + AddressFormat string `json:"address_format"` + SupportsEstimateFee bool `json:"supports_estimate_fee"` + SupportsEstimateSmartFee bool `json:"supports_estimate_smart_fee"` + XPubMagic uint32 `json:"xpub_magic,omitempty"` + XPubMagicSegwitP2sh uint32 `json:"xpub_magic_segwit_p2sh,omitempty"` + XPubMagicSegwitNative uint32 `json:"xpub_magic_segwit_native,omitempty"` + Slip44 uint32 `json:"slip44,omitempty"` + AlternativeEstimateFee string `json:"alternativeEstimateFee,omitempty"` + AlternativeEstimateFeeParams string `json:"alternativeEstimateFeeParams,omitempty"` } // NewBitcoinRPC returns new BitcoinRPC instance. @@ -127,6 +129,14 @@ func (b *BitcoinRPC) Initialize() error { glog.Info("rpc: block chain ", params.Name) + if b.ChainConfig.AlternativeEstimateFee == "whatthefee" { + if err = InitWhatTheFee(b, b.ChainConfig.AlternativeEstimateFeeParams); err != nil { + glog.Error("InitWhatTheFee error ", err, " Reverting to default estimateFee functionality") + // disable AlternativeEstimateFee logic + b.ChainConfig.AlternativeEstimateFee = "" + } + } + return nil } diff --git a/bchain/coins/btc/whatthefee.go b/bchain/coins/btc/whatthefee.go new file mode 100644 index 00000000..ae572b9c --- /dev/null +++ b/bchain/coins/btc/whatthefee.go @@ -0,0 +1,129 @@ +package btc + +import ( + "blockbook/bchain" + "bytes" + "encoding/json" + "math" + "net/http" + "strconv" + "sync" + "time" + + "github.com/golang/glog" + + "github.com/juju/errors" +) + +// https://whatthefee.io returns +// {"index": [3, 6, 9, 12, 18, 24, 36, 48, 72, 96, 144], +// "columns": ["0.0500", "0.2000", "0.5000", "0.8000", "0.9500"], +// "data": [[60, 180, 280, 400, 440], [20, 120, 180, 380, 440], +// [0, 120, 160, 360, 420], [0, 80, 160, 300, 380], [0, 20, 120, 220, 360], +// [0, 20, 100, 180, 300], [0, 0, 80, 140, 240], [0, 0, 60, 100, 180], +// [0, 0, 40, 60, 140], [0, 0, 20, 20, 60], [0, 0, 0, 0, 20]]} + +type whatTheFeeServiceResult struct { + Index []int `json:"index"` + Columns []string `json:"columns"` + Data [][]int `json:"data"` +} + +type whatTheFeeParams struct { + URL string `json:"url"` + PeriodSeconds int `periodSeconds:"url"` +} + +type whatTheFeeFee struct { + blocks int + feesPerKB []int +} + +type whatTheFeeData struct { + params whatTheFeeParams + probabilities []string + fees []whatTheFeeFee + lastSync time.Time + chain bchain.BlockChain + mux sync.Mutex +} + +var whatTheFee whatTheFeeData + +// InitWhatTheFee initializes https://whatthefee.io handler +func InitWhatTheFee(chain bchain.BlockChain, params string) error { + err := json.Unmarshal([]byte(params), &whatTheFee.params) + if err != nil { + return err + } + if whatTheFee.params.URL == "" || whatTheFee.params.PeriodSeconds == 0 { + return errors.New("Missing parameters") + } + whatTheFee.chain = chain + go whatTheFeeDownloader() + return nil +} + +func whatTheFeeDownloader() { + period := time.Duration(whatTheFee.params.PeriodSeconds) * time.Second + timer := time.NewTimer(period) + for { + var data whatTheFeeServiceResult + err := whatTheFeeGetData(&data) + if err != nil { + glog.Error("whatTheFeeGetData ", err) + } else { + whatTheFeeProcessData(&data) + } + <-timer.C + timer.Reset(period) + } +} + +func whatTheFeeProcessData(data *whatTheFeeServiceResult) { + if len(data.Index) == 0 || len(data.Index) != len(data.Data) || len(data.Columns) == 0 { + glog.Errorf("invalid data %+v", data) + return + } + whatTheFee.mux.Lock() + defer whatTheFee.mux.Unlock() + whatTheFee.probabilities = data.Columns + whatTheFee.fees = make([]whatTheFeeFee, len(data.Index)) + for i, blocks := range data.Index { + if len(data.Columns) != len(data.Data[i]) { + glog.Errorf("invalid data %+v", data) + return + } + fees := make([]int, len(data.Columns)) + for j, l := range data.Data[i] { + fees[j] = int(1000 * math.Exp(float64(l)/100)) + } + whatTheFee.fees[i] = whatTheFeeFee{ + blocks: blocks, + feesPerKB: fees, + } + } + whatTheFee.lastSync = time.Now() + glog.Infof("%+v", whatTheFee.fees) +} + +func whatTheFeeGetData(res interface{}) error { + var httpData []byte + httpReq, err := http.NewRequest("GET", whatTheFee.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 server returns HTTP error code it might not return json with response + // handle both cases + if httpRes.StatusCode != 200 { + return errors.New("whatthefee.io returned status " + strconv.Itoa(httpRes.StatusCode)) + } + return safeDecodeResponse(httpRes.Body, &res) +}