|
package priceupdater
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/dghubble/sling"
|
|
ethCommon "github.com/ethereum/go-ethereum/common"
|
|
"github.com/hermeznetwork/hermez-node/common"
|
|
"github.com/hermeznetwork/hermez-node/db/historydb"
|
|
"github.com/hermeznetwork/hermez-node/log"
|
|
"github.com/hermeznetwork/tracerr"
|
|
)
|
|
|
|
const (
|
|
defaultMaxIdleConns = 10
|
|
defaultIdleConnTimeout = 2 * time.Second
|
|
)
|
|
|
|
// UpdateMethodType defines the token price update mechanism
|
|
type UpdateMethodType string
|
|
|
|
const (
|
|
// UpdateMethodTypeBitFinexV2 is the http API used by bitfinex V2
|
|
UpdateMethodTypeBitFinexV2 UpdateMethodType = "bitfinexV2"
|
|
// UpdateMethodTypeCoingeckoV3 is the http API used by copingecko V3
|
|
UpdateMethodTypeCoingeckoV3 UpdateMethodType = "coingeckoV3"
|
|
// UpdateMethodTypeStatic is the value given by the configuration
|
|
UpdateMethodTypeStatic UpdateMethodType = "static"
|
|
// UpdateMethodTypeIgnore indicates to not update the value, to set value 0
|
|
// it's better to use UpdateMethodTypeStatic
|
|
UpdateMethodTypeIgnore UpdateMethodType = "ignore"
|
|
)
|
|
|
|
func (t *UpdateMethodType) valid() bool {
|
|
switch *t {
|
|
case UpdateMethodTypeBitFinexV2:
|
|
return true
|
|
case UpdateMethodTypeCoingeckoV3:
|
|
return true
|
|
case UpdateMethodTypeStatic:
|
|
return true
|
|
case UpdateMethodTypeIgnore:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// TokenConfig specifies how a single token get its price updated
|
|
type TokenConfig struct {
|
|
UpdateMethod UpdateMethodType
|
|
StaticValue float64 // required by UpdateMethodTypeStatic
|
|
Symbol string
|
|
Addr ethCommon.Address
|
|
}
|
|
|
|
func (t *TokenConfig) valid() bool {
|
|
if (t.Addr == common.EmptyAddr && t.Symbol != "ETH") ||
|
|
(t.Symbol == "" && t.UpdateMethod == UpdateMethodTypeBitFinexV2) {
|
|
return false
|
|
}
|
|
return t.UpdateMethod.valid()
|
|
}
|
|
|
|
// PriceUpdater definition
|
|
type PriceUpdater struct {
|
|
db *historydb.HistoryDB
|
|
defaultUpdateMethod UpdateMethodType
|
|
tokensList []historydb.TokenSymbolAndAddr
|
|
tokensConfig map[ethCommon.Address]TokenConfig
|
|
clientCoingeckoV3 *sling.Sling
|
|
clientBitfinexV2 *sling.Sling
|
|
}
|
|
|
|
// NewPriceUpdater is the constructor for the updater
|
|
func NewPriceUpdater(
|
|
defaultUpdateMethodType UpdateMethodType,
|
|
tokensConfig []TokenConfig,
|
|
db *historydb.HistoryDB,
|
|
bitfinexV2URL, coingeckoV3URL string,
|
|
) (*PriceUpdater, error) {
|
|
// Validate params
|
|
if !defaultUpdateMethodType.valid() || defaultUpdateMethodType == UpdateMethodTypeStatic {
|
|
return nil, tracerr.Wrap(
|
|
fmt.Errorf("Invalid defaultUpdateMethodType: %v", defaultUpdateMethodType),
|
|
)
|
|
}
|
|
tokensConfigMap := make(map[ethCommon.Address]TokenConfig)
|
|
for _, t := range tokensConfig {
|
|
if !t.valid() {
|
|
return nil, tracerr.Wrap(fmt.Errorf("Invalid tokensConfig, wrong entry: %+v", t))
|
|
}
|
|
tokensConfigMap[t.Addr] = t
|
|
}
|
|
// Init
|
|
tr := &http.Transport{
|
|
MaxIdleConns: defaultMaxIdleConns,
|
|
IdleConnTimeout: defaultIdleConnTimeout,
|
|
DisableCompression: true,
|
|
}
|
|
httpClient := &http.Client{Transport: tr}
|
|
return &PriceUpdater{
|
|
db: db,
|
|
defaultUpdateMethod: defaultUpdateMethodType,
|
|
tokensList: []historydb.TokenSymbolAndAddr{},
|
|
tokensConfig: tokensConfigMap,
|
|
clientCoingeckoV3: sling.New().Base(coingeckoV3URL).Client(httpClient),
|
|
clientBitfinexV2: sling.New().Base(bitfinexV2URL).Client(httpClient),
|
|
}, nil
|
|
}
|
|
|
|
func (p *PriceUpdater) getTokenPriceBitfinex(ctx context.Context, tokenSymbol string) (float64, error) {
|
|
state := [10]float64{}
|
|
url := "ticker/t" + tokenSymbol + "USD"
|
|
req, err := p.clientBitfinexV2.New().Get(url).Request()
|
|
if err != nil {
|
|
return 0, tracerr.Wrap(err)
|
|
}
|
|
res, err := p.clientBitfinexV2.Do(req.WithContext(ctx), &state, nil)
|
|
if err != nil {
|
|
return 0, tracerr.Wrap(err)
|
|
}
|
|
if res.StatusCode != http.StatusOK {
|
|
return 0, tracerr.Wrap(fmt.Errorf("http response is not is %v", res.StatusCode))
|
|
}
|
|
return state[6], nil
|
|
}
|
|
|
|
func (p *PriceUpdater) getTokenPriceCoingecko(ctx context.Context, tokenAddr ethCommon.Address) (float64, error) {
|
|
responseObject := make(map[string]map[string]float64)
|
|
var url string
|
|
var id string
|
|
if tokenAddr == common.EmptyAddr { // Special case for Ether
|
|
url = "simple/price?ids=ethereum&vs_currencies=usd"
|
|
id = "ethereum"
|
|
} else { // Common case (ERC20)
|
|
id = strings.ToLower(tokenAddr.String())
|
|
url = "simple/token_price/ethereum?contract_addresses=" +
|
|
id + "&vs_currencies=usd"
|
|
}
|
|
req, err := p.clientCoingeckoV3.New().Get(url).Request()
|
|
if err != nil {
|
|
return 0, tracerr.Wrap(err)
|
|
}
|
|
res, err := p.clientCoingeckoV3.Do(req.WithContext(ctx), &responseObject, nil)
|
|
if err != nil {
|
|
return 0, tracerr.Wrap(err)
|
|
}
|
|
if res.StatusCode != http.StatusOK {
|
|
return 0, tracerr.Wrap(fmt.Errorf("http response is not is %v", res.StatusCode))
|
|
}
|
|
price := responseObject[id]["usd"]
|
|
if price <= 0 {
|
|
return 0, tracerr.Wrap(fmt.Errorf("price not found for %v", id))
|
|
}
|
|
return price, nil
|
|
}
|
|
|
|
// UpdatePrices is triggered by the Coordinator, and internally will update the
|
|
// token prices in the db
|
|
func (p *PriceUpdater) UpdatePrices(ctx context.Context) {
|
|
for _, token := range p.tokensConfig {
|
|
var tokenPrice float64
|
|
var err error
|
|
switch token.UpdateMethod {
|
|
case UpdateMethodTypeBitFinexV2:
|
|
tokenPrice, err = p.getTokenPriceBitfinex(ctx, token.Symbol)
|
|
case UpdateMethodTypeCoingeckoV3:
|
|
tokenPrice, err = p.getTokenPriceCoingecko(ctx, token.Addr)
|
|
case UpdateMethodTypeStatic:
|
|
tokenPrice = token.StaticValue
|
|
if tokenPrice == float64(0) {
|
|
log.Warn("token price is set to 0. Probably StaticValue is not put in the configuration file")
|
|
}
|
|
case UpdateMethodTypeIgnore:
|
|
continue
|
|
}
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
if err != nil {
|
|
log.Warnw("token price not updated (get error)",
|
|
"err", err, "token", token.Symbol, "updateMethod", token.UpdateMethod)
|
|
}
|
|
if err = p.db.UpdateTokenValue(token.Addr, tokenPrice); err != nil {
|
|
log.Errorw("token price not updated (db error)",
|
|
"err", err, "token", token.Symbol, "updateMethod", token.UpdateMethod)
|
|
}
|
|
}
|
|
}
|
|
|
|
// UpdateTokenList get the registered token symbols from HistoryDB
|
|
func (p *PriceUpdater) UpdateTokenList() error {
|
|
dbTokens, err := p.db.GetTokenSymbolsAndAddrs()
|
|
if err != nil {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
// For each token from the DB
|
|
for _, dbToken := range dbTokens {
|
|
// If the token doesn't exists in the config list,
|
|
// add it with default update emthod
|
|
if _, ok := p.tokensConfig[dbToken.Addr]; !ok {
|
|
p.tokensConfig[dbToken.Addr] = TokenConfig{
|
|
UpdateMethod: p.defaultUpdateMethod,
|
|
Symbol: dbToken.Symbol,
|
|
Addr: dbToken.Addr,
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|