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.

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