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 "token", token.Symbol)
179 }
180 case UpdateMethodTypeIgnore:
181 continue
182 }
183 if ctx.Err() != nil {
184 return
185 }
186 if err != nil {
187 log.Warnw("token price not updated (get error)",
188 "err", err, "token", token.Symbol, "updateMethod", token.UpdateMethod)
189 }
190 if err = p.db.UpdateTokenValue(token.Addr, tokenPrice); err != nil {
191 log.Errorw("token price not updated (db error)",
192 "err", err, "token", token.Symbol, "updateMethod", token.UpdateMethod)
193 }
194 }
195}
196
197// UpdateTokenList get the registered token symbols from HistoryDB
198func (p *PriceUpdater) UpdateTokenList() error {
199 dbTokens, err := p.db.GetTokenSymbolsAndAddrs()
200 if err != nil {
201 return tracerr.Wrap(err)
202 }
203 // For each token from the DB
204 for _, dbToken := range dbTokens {
205 // If the token doesn't exists in the config list,
206 // add it with default update emthod
207 if _, ok := p.tokensConfig[dbToken.Addr]; !ok {
208 p.tokensConfig[dbToken.Addr] = TokenConfig{
209 UpdateMethod: p.defaultUpdateMethod,
210 Symbol: dbToken.Symbol,
211 Addr: dbToken.Addr,
212 }
213 }
214 }
215 return nil
216}