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// APIType defines the token exchange API
 24type APIType string
 25
 26const (
 27	// APITypeBitFinexV2 is the http API used by bitfinex V2
 28	APITypeBitFinexV2 APIType = "bitfinexV2"
 29	// APITypeCoingeckoV3 is the http API used by copingecko V3
 30	APITypeCoingeckoV3 APIType = "coingeckoV3"
 31)
 32
 33func (t *APIType) valid() bool {
 34	switch *t {
 35	case APITypeBitFinexV2:
 36		return true
 37	case APITypeCoingeckoV3:
 38		return true
 39	default:
 40		return false
 41	}
 42}
 43
 44// PriceUpdater definition
 45type PriceUpdater struct {
 46	db      *historydb.HistoryDB
 47	apiURL  string
 48	apiType APIType
 49	tokens  []historydb.TokenSymbolAndAddr
 50}
 51
 52// NewPriceUpdater is the constructor for the updater
 53func NewPriceUpdater(apiURL string, apiType APIType, db *historydb.HistoryDB) (*PriceUpdater,
 54	error) {
 55	if !apiType.valid() {
 56		return nil, tracerr.Wrap(fmt.Errorf("Invalid apiType: %v", apiType))
 57	}
 58	return &PriceUpdater{
 59		db:      db,
 60		apiURL:  apiURL,
 61		apiType: apiType,
 62		tokens:  []historydb.TokenSymbolAndAddr{},
 63	}, nil
 64}
 65
 66func getTokenPriceBitfinex(ctx context.Context, client *sling.Sling,
 67	tokenSymbol string) (float64, error) {
 68	state := [10]float64{}
 69	req, err := client.New().Get("ticker/t" + tokenSymbol + "USD").Request()
 70	if err != nil {
 71		return 0, tracerr.Wrap(err)
 72	}
 73	res, err := client.Do(req.WithContext(ctx), &state, nil)
 74	if err != nil {
 75		return 0, tracerr.Wrap(err)
 76	}
 77	if res.StatusCode != http.StatusOK {
 78		return 0, tracerr.Wrap(fmt.Errorf("http response is not is %v", res.StatusCode))
 79	}
 80	return state[6], nil
 81}
 82
 83func getTokenPriceCoingecko(ctx context.Context, client *sling.Sling,
 84	tokenAddr ethCommon.Address) (float64, error) {
 85	responseObject := make(map[string]map[string]float64)
 86	var url string
 87	var id string
 88	if tokenAddr == common.EmptyAddr { // Special case for Ether
 89		url = "simple/price?ids=ethereum&vs_currencies=usd"
 90		id = "ethereum"
 91	} else { // Common case (ERC20)
 92		id = strings.ToLower(tokenAddr.String())
 93		url = "simple/token_price/ethereum?contract_addresses=" +
 94			id + "&vs_currencies=usd"
 95	}
 96	req, err := client.New().Get(url).Request()
 97	if err != nil {
 98		return 0, tracerr.Wrap(err)
 99	}
100	res, err := client.Do(req.WithContext(ctx), &responseObject, nil)
101	if err != nil {
102		return 0, tracerr.Wrap(err)
103	}
104	if res.StatusCode != http.StatusOK {
105		return 0, tracerr.Wrap(fmt.Errorf("http response is not is %v", res.StatusCode))
106	}
107	price := responseObject[id]["usd"]
108	if price <= 0 {
109		return 0, tracerr.Wrap(fmt.Errorf("price not found for %v", id))
110	}
111	return price, nil
112}
113
114// UpdatePrices is triggered by the Coordinator, and internally will update the
115// token prices in the db
116func (p *PriceUpdater) UpdatePrices(ctx context.Context) {
117	tr := &http.Transport{
118		MaxIdleConns:       defaultMaxIdleConns,
119		IdleConnTimeout:    defaultIdleConnTimeout,
120		DisableCompression: true,
121	}
122	httpClient := &http.Client{Transport: tr}
123	client := sling.New().Base(p.apiURL).Client(httpClient)
124
125	for _, token := range p.tokens {
126		var tokenPrice float64
127		var err error
128		switch p.apiType {
129		case APITypeBitFinexV2:
130			tokenPrice, err = getTokenPriceBitfinex(ctx, client, token.Symbol)
131		case APITypeCoingeckoV3:
132			tokenPrice, err = getTokenPriceCoingecko(ctx, client, token.Addr)
133		}
134		if ctx.Err() != nil {
135			return
136		}
137		if err != nil {
138			log.Warnw("token price not updated (get error)",
139				"err", err, "token", token.Symbol, "apiType", p.apiType)
140		}
141		if err = p.db.UpdateTokenValue(token.Symbol, tokenPrice); err != nil {
142			log.Errorw("token price not updated (db error)",
143				"err", err, "token", token.Symbol, "apiType", p.apiType)
144		}
145	}
146}
147
148// UpdateTokenList get the registered token symbols from HistoryDB
149func (p *PriceUpdater) UpdateTokenList() error {
150	tokens, err := p.db.GetTokenSymbolsAndAddrs()
151	if err != nil {
152		return tracerr.Wrap(err)
153	}
154	p.tokens = tokens
155	return nil
156}