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}