|
|
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 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 }
|