You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

216 lines
6.5 KiB

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