Add coingecko client to price updater

This commit is contained in:
arnaubennassar
2021-03-08 15:06:42 +01:00
parent 1778b08043
commit ac66ede917
6 changed files with 111 additions and 52 deletions

View File

@@ -4,9 +4,12 @@ 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"
@@ -23,12 +26,16 @@ type APIType string
const (
// APITypeBitFinexV2 is the http API used by bitfinex V2
APITypeBitFinexV2 APIType = "bitfinexV2"
// APITypeCoingeckoV3 is the http API used by copingecko V3
APITypeCoingeckoV3 APIType = "coingeckoV3"
)
func (t *APIType) valid() bool {
switch *t {
case APITypeBitFinexV2:
return true
case APITypeCoingeckoV3:
return true
default:
return false
}
@@ -36,24 +43,23 @@ func (t *APIType) valid() bool {
// PriceUpdater definition
type PriceUpdater struct {
db *historydb.HistoryDB
apiURL string
apiType APIType
tokenSymbols []string
db *historydb.HistoryDB
apiURL string
apiType APIType
tokens []historydb.TokenSymbolAndAddr
}
// NewPriceUpdater is the constructor for the updater
func NewPriceUpdater(apiURL string, apiType APIType, db *historydb.HistoryDB) (*PriceUpdater,
error) {
tokenSymbols := []string{}
if !apiType.valid() {
return nil, tracerr.Wrap(fmt.Errorf("Invalid apiType: %v", apiType))
}
return &PriceUpdater{
db: db,
apiURL: apiURL,
apiType: apiType,
tokenSymbols: tokenSymbols,
db: db,
apiURL: apiURL,
apiType: apiType,
tokens: []historydb.TokenSymbolAndAddr{},
}, nil
}
@@ -74,6 +80,37 @@ func getTokenPriceBitfinex(ctx context.Context, client *sling.Sling,
return state[6], nil
}
func getTokenPriceCoingecko(ctx context.Context, client *sling.Sling,
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 := client.New().Get(url).Request()
if err != nil {
return 0, tracerr.Wrap(err)
}
res, err := client.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) {
@@ -85,33 +122,35 @@ func (p *PriceUpdater) UpdatePrices(ctx context.Context) {
httpClient := &http.Client{Transport: tr}
client := sling.New().Base(p.apiURL).Client(httpClient)
for _, tokenSymbol := range p.tokenSymbols {
for _, token := range p.tokens {
var tokenPrice float64
var err error
switch p.apiType {
case APITypeBitFinexV2:
tokenPrice, err = getTokenPriceBitfinex(ctx, client, tokenSymbol)
tokenPrice, err = getTokenPriceBitfinex(ctx, client, token.Symbol)
case APITypeCoingeckoV3:
tokenPrice, err = getTokenPriceCoingecko(ctx, client, token.Addr)
}
if ctx.Err() != nil {
return
}
if err != nil {
log.Warnw("token price not updated (get error)",
"err", err, "token", tokenSymbol, "apiType", p.apiType)
"err", err, "token", token.Symbol, "apiType", p.apiType)
}
if err = p.db.UpdateTokenValue(tokenSymbol, tokenPrice); err != nil {
if err = p.db.UpdateTokenValue(token.Symbol, tokenPrice); err != nil {
log.Errorw("token price not updated (db error)",
"err", err, "token", tokenSymbol, "apiType", p.apiType)
"err", err, "token", token.Symbol, "apiType", p.apiType)
}
}
}
// UpdateTokenList get the registered token symbols from HistoryDB
func (p *PriceUpdater) UpdateTokenList() error {
tokenSymbols, err := p.db.GetTokenSymbols()
tokens, err := p.db.GetTokenSymbolsAndAddrs()
if err != nil {
return tracerr.Wrap(err)
}
p.tokenSymbols = tokenSymbols
p.tokens = tokens
return nil
}

View File

@@ -2,7 +2,6 @@ package priceupdater
import (
"context"
"math/big"
"os"
"testing"
@@ -15,29 +14,45 @@ import (
"github.com/stretchr/testify/require"
)
func TestPriceUpdater(t *testing.T) {
var historyDB *historydb.HistoryDB
func TestMain(m *testing.M) {
// Init DB
pass := os.Getenv("POSTGRES_PASS")
db, err := dbUtils.InitSQLDB(5432, "localhost", "hermez", pass, "hermez")
assert.NoError(t, err)
historyDB := historydb.NewHistoryDB(db, db, nil)
if err != nil {
panic(err)
}
historyDB = historydb.NewHistoryDB(db, db, nil)
// Clean DB
test.WipeDB(historyDB.DB())
// Populate DB
// Gen blocks and add them to DB
blocks := test.GenBlocks(1, 2)
assert.NoError(t, historyDB.AddBlocks(blocks))
err = historyDB.AddBlocks(blocks)
if err != nil {
panic(err)
}
// Gen tokens and add them to DB
tokens := []common.Token{}
tokens = append(tokens, common.Token{
TokenID: 1,
EthBlockNum: blocks[0].Num,
EthAddr: ethCommon.BigToAddress(big.NewInt(2)),
EthAddr: ethCommon.HexToAddress("0x6b175474e89094c44da98b954eedeac495271d0f"),
Name: "DAI",
Symbol: "DAI",
Decimals: 18,
})
assert.NoError(t, historyDB.AddTokens(tokens))
err = historyDB.AddTokens(tokens)
if err != nil {
panic(err)
}
result := m.Run()
os.Exit(result)
}
func TestPriceUpdaterBitfinex(t *testing.T) {
// Init price updater
pu, err := NewPriceUpdater("https://api-pub.bitfinex.com/v2/", APITypeBitFinexV2, historyDB)
require.NoError(t, err)
@@ -45,13 +60,29 @@ func TestPriceUpdater(t *testing.T) {
assert.NoError(t, pu.UpdateTokenList())
// Update prices
pu.UpdatePrices(context.Background())
assertTokenHasPriceAndClean(t)
}
func TestPriceUpdaterCoingecko(t *testing.T) {
// Init price updater
pu, err := NewPriceUpdater("https://api.coingecko.com/api/v3/", APITypeCoingeckoV3, historyDB)
require.NoError(t, err)
// Update token list
assert.NoError(t, pu.UpdateTokenList())
// Update prices
pu.UpdatePrices(context.Background())
assertTokenHasPriceAndClean(t)
}
func assertTokenHasPriceAndClean(t *testing.T) {
// Check that prices have been updated
fetchedTokens, err := historyDB.GetTokensTest()
require.NoError(t, err)
// TokenID 0 (ETH) is always on the DB
assert.Equal(t, 2, len(fetchedTokens))
for _, token := range fetchedTokens {
assert.NotNil(t, token.USD)
assert.NotNil(t, token.USDUpdate)
require.NotNil(t, token.USD)
require.NotNil(t, token.USDUpdate)
assert.Greater(t, *token.USD, 0.0)
}
}