arnaucube
arnaucube/hermez-nodePublic Self-hosted repository
hermez-node/priceupdater/priceupdater.go
priceupdater/priceupdater.goRaw
  1package priceupdater
  2
  3import (
  4	"context"
  5	"fmt"
  6	"net/http"
  7	"strings"
  8	"time"
  9
 10	"github.com/dghubble/sling"
 11	ethCommon "github.com/ethereum/go-ethereum/common"
 12	"github.com/hermeznetwork/hermez-node/common"
 13	"github.com/hermeznetwork/hermez-node/db/historydb"
 14	"github.com/hermeznetwork/hermez-node/log"
 15	"github.com/hermeznetwork/tracerr"
 16)
 17
 18const (
 19	defaultMaxIdleConns    = 10
 20	defaultIdleConnTimeout = 2 * time.Second
 21)
 22
 23// UpdateMethodType defines the token price update mechanism
 24type UpdateMethodType string
 25
 26const (
 27	// UpdateMethodTypeBitFinexV2 is the http API used by bitfinex V2
 28	UpdateMethodTypeBitFinexV2 UpdateMethodType = "bitfinexV2"
 29	// UpdateMethodTypeCoingeckoV3 is the http API used by copingecko V3
 30	UpdateMethodTypeCoingeckoV3 UpdateMethodType = "coingeckoV3"
 31	// UpdateMethodTypeStatic is the value given by the configuration
 32	UpdateMethodTypeStatic UpdateMethodType = "static"
 33	// UpdateMethodTypeIgnore indicates to not update the value, to set value 0
 34	// it's better to use UpdateMethodTypeStatic
 35	UpdateMethodTypeIgnore UpdateMethodType = "ignore"
 36)
 37
 38func (t *UpdateMethodType) valid() bool {
 39	switch *t {
 40	case UpdateMethodTypeBitFinexV2:
 41		return true
 42	case UpdateMethodTypeCoingeckoV3:
 43		return true
 44	case UpdateMethodTypeStatic:
 45		return true
 46	case UpdateMethodTypeIgnore:
 47		return true
 48	default:
 49		return false
 50	}
 51}
 52
 53// TokenConfig specifies how a single token get its price updated
 54type TokenConfig struct {
 55	UpdateMethod UpdateMethodType
 56	StaticValue  float64 // required by UpdateMethodTypeStatic
 57	Symbol       string
 58	Addr         ethCommon.Address
 59}
 60
 61func (t *TokenConfig) valid() bool {
 62	if (t.Addr == common.EmptyAddr && t.Symbol != "ETH") ||
 63		(t.Symbol == "" && t.UpdateMethod == UpdateMethodTypeBitFinexV2) {
 64		return false
 65	}
 66	return t.UpdateMethod.valid()
 67}
 68
 69// PriceUpdater definition
 70type PriceUpdater struct {
 71	db                  *historydb.HistoryDB
 72	defaultUpdateMethod UpdateMethodType
 73	tokensList          []historydb.TokenSymbolAndAddr
 74	tokensConfig        map[ethCommon.Address]TokenConfig
 75	clientCoingeckoV3   *sling.Sling
 76	clientBitfinexV2    *sling.Sling
 77}
 78
 79// NewPriceUpdater is the constructor for the updater
 80func NewPriceUpdater(
 81	defaultUpdateMethodType UpdateMethodType,
 82	tokensConfig []TokenConfig,
 83	db *historydb.HistoryDB,
 84	bitfinexV2URL, coingeckoV3URL string,
 85) (*PriceUpdater, error) {
 86	// Validate params
 87	if !defaultUpdateMethodType.valid() || defaultUpdateMethodType == UpdateMethodTypeStatic {
 88		return nil, tracerr.Wrap(
 89			fmt.Errorf("Invalid defaultUpdateMethodType: %v", defaultUpdateMethodType),
 90		)
 91	}
 92	tokensConfigMap := make(map[ethCommon.Address]TokenConfig)
 93	for _, t := range tokensConfig {
 94		if !t.valid() {
 95			return nil, tracerr.Wrap(fmt.Errorf("Invalid tokensConfig, wrong entry: %+v", t))
 96		}
 97		tokensConfigMap[t.Addr] = t
 98	}
 99	// Init
100	tr := &http.Transport{
101		MaxIdleConns:       defaultMaxIdleConns,
102		IdleConnTimeout:    defaultIdleConnTimeout,
103		DisableCompression: true,
104	}
105	httpClient := &http.Client{Transport: tr}
106	return &PriceUpdater{
107		db:                  db,
108		defaultUpdateMethod: defaultUpdateMethodType,
109		tokensList:          []historydb.TokenSymbolAndAddr{},
110		tokensConfig:        tokensConfigMap,
111		clientCoingeckoV3:   sling.New().Base(coingeckoV3URL).Client(httpClient),
112		clientBitfinexV2:    sling.New().Base(bitfinexV2URL).Client(httpClient),
113	}, nil
114}
115
116func (p *PriceUpdater) getTokenPriceBitfinex(ctx context.Context, tokenSymbol string) (float64, error) {
117	state := [10]float64{}
118	url := "ticker/t" + tokenSymbol + "USD"
119	req, err := p.clientBitfinexV2.New().Get(url).Request()
120	if err != nil {
121		return 0, tracerr.Wrap(err)
122	}
123	res, err := p.clientBitfinexV2.Do(req.WithContext(ctx), &state, nil)
124	if err != nil {
125		return 0, tracerr.Wrap(err)
126	}
127	if res.StatusCode != http.StatusOK {
128		return 0, tracerr.Wrap(fmt.Errorf("http response is not is %v", res.StatusCode))
129	}
130	return state[6], nil
131}
132
133func (p *PriceUpdater) getTokenPriceCoingecko(ctx context.Context, tokenAddr ethCommon.Address) (float64, error) {
134	responseObject := make(map[string]map[string]float64)
135	var url string
136	var id string
137	if tokenAddr == common.EmptyAddr { // Special case for Ether
138		url = "simple/price?ids=ethereum&vs_currencies=usd"
139		id = "ethereum"
140	} else { // Common case (ERC20)
141		id = strings.ToLower(tokenAddr.String())
142		url = "simple/token_price/ethereum?contract_addresses=" +
143			id + "&vs_currencies=usd"
144	}
145	req, err := p.clientCoingeckoV3.New().Get(url).Request()
146	if err != nil {
147		return 0, tracerr.Wrap(err)
148	}
149	res, err := p.clientCoingeckoV3.Do(req.WithContext(ctx), &responseObject, nil)
150	if err != nil {
151		return 0, tracerr.Wrap(err)
152	}
153	if res.StatusCode != http.StatusOK {
154		return 0, tracerr.Wrap(fmt.Errorf("http response is not is %v", res.StatusCode))
155	}
156	price := responseObject[id]["usd"]
157	if price <= 0 {
158		return 0, tracerr.Wrap(fmt.Errorf("price not found for %v", id))
159	}
160	return price, nil
161}
162
163// UpdatePrices is triggered by the Coordinator, and internally will update the
164// token prices in the db
165func (p *PriceUpdater) UpdatePrices(ctx context.Context) {
166	for _, token := range p.tokensConfig {
167		var tokenPrice float64
168		var err error
169		switch token.UpdateMethod {
170		case UpdateMethodTypeBitFinexV2:
171			tokenPrice, err = p.getTokenPriceBitfinex(ctx, token.Symbol)
172		case UpdateMethodTypeCoingeckoV3:
173			tokenPrice, err = p.getTokenPriceCoingecko(ctx, token.Addr)
174		case UpdateMethodTypeStatic:
175			tokenPrice = token.StaticValue
176			if tokenPrice == float64(0) {
177				log.Warn("token price is set to 0. Probably StaticValue is not put in the configuration file")
178			}
179		case UpdateMethodTypeIgnore:
180			continue
181		}
182		if ctx.Err() != nil {
183			return
184		}
185		if err != nil {
186			log.Warnw("token price not updated (get error)",
187				"err", err, "token", token.Symbol, "updateMethod", token.UpdateMethod)
188		}
189		if err = p.db.UpdateTokenValue(token.Addr, tokenPrice); err != nil {
190			log.Errorw("token price not updated (db error)",
191				"err", err, "token", token.Symbol, "updateMethod", token.UpdateMethod)
192		}
193	}
194}
195
196// UpdateTokenList get the registered token symbols from HistoryDB
197func (p *PriceUpdater) UpdateTokenList() error {
198	dbTokens, err := p.db.GetTokenSymbolsAndAddrs()
199	if err != nil {
200		return tracerr.Wrap(err)
201	}
202	// For each token from the DB
203	for _, dbToken := range dbTokens {
204		// If the token doesn't exists in the config list,
205		// add it with default update emthod
206		if _, ok := p.tokensConfig[dbToken.Addr]; !ok {
207			p.tokensConfig[dbToken.Addr] = TokenConfig{
208				UpdateMethod: p.defaultUpdateMethod,
209				Symbol:       dbToken.Symbol,
210				Addr:         dbToken.Addr,
211			}
212		}
213	}
214	return nil
215}