github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/foreign-scrapers/CoinMarketCapScraper.go (about)

     1  package foreignscrapers
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"strings"
    13  	"time"
    14  
    15  	models "github.com/diadata-org/diadata/pkg/model"
    16  	"github.com/diadata-org/diadata/pkg/utils"
    17  	log "github.com/sirupsen/logrus"
    18  )
    19  
    20  const (
    21  	coinMarketCapRefreshDelay = time.Second * 60 * 2
    22  	coinMarketCapsource       = "CoinMarketCap"
    23  )
    24  
    25  type Quote struct {
    26  	Price            float64 `json:"price"`
    27  	Volume24H        float64 `json:"volume_24h"`
    28  	PercentChange1H  float64 `json:"percent_change_1h"`
    29  	PercentChange24H float64 `json:"percent_change_24h"`
    30  	PercentChange7D  float64 `json:"percent_change_7d"`
    31  	MarketCap        float64 `json:"market_cap"`
    32  	LastUpdated      string  `json:"last_updated"`
    33  }
    34  type CoinMarketCapListing struct {
    35  	Data []struct {
    36  		ID                int              `json:"id"`
    37  		Name              string           `json:"name"`
    38  		Symbol            string           `json:"symbol"`
    39  		Slug              string           `json:"slug"`
    40  		CmcRank           int              `json:"cmc_rank"`
    41  		NumMarketPairs    int              `json:"num_market_pairs"`
    42  		CirculatingSupply float64          `json:"circulating_supply"`
    43  		TotalSupply       float64          `json:"total_supply"`
    44  		MaxSupply         float64          `json:"max_supply"`
    45  		LastUpdated       string           `json:"last_updated"`
    46  		DateAdded         string           `json:"date_added"`
    47  		Tags              []string         `json:"tags"`
    48  		Platform          interface{}      `json:"platform"`
    49  		Quote             map[string]Quote `json:"quote"`
    50  	} `json:"data"`
    51  	Status struct {
    52  		Timestamp    time.Time `json:"timestamp"`
    53  		ErrorCode    int       `json:"error_code"`
    54  		ErrorMessage string    `json:"error_message"`
    55  		Elapsed      int       `json:"elapsed"`
    56  		CreditCount  int       `json:"credit_count"`
    57  	} `json:"status"`
    58  }
    59  
    60  type CoinMarketCapScraper struct {
    61  	ticker          *time.Ticker
    62  	foreignScrapper ForeignScraper
    63  }
    64  
    65  func NewCoinMarketCapScraper(datastore models.Datastore) *CoinMarketCapScraper {
    66  
    67  	foreignScrapper := ForeignScraper{
    68  		shutdown:      make(chan nothing),
    69  		error:         nil,
    70  		datastore:     datastore,
    71  		chanQuotation: make(chan *models.ForeignQuotation),
    72  	}
    73  	s := &CoinMarketCapScraper{
    74  		ticker:          time.NewTicker(coinMarketCapRefreshDelay),
    75  		foreignScrapper: foreignScrapper,
    76  	}
    77  	go s.mainLoop()
    78  
    79  	return s
    80  
    81  }
    82  
    83  // mainLoop runs in a goroutine until channel s is closed.
    84  func (scraper *CoinMarketCapScraper) mainLoop() {
    85  	for {
    86  		select {
    87  		case <-scraper.ticker.C:
    88  			err := scraper.UpdateQuotation()
    89  			if err != nil {
    90  				log.Error(err)
    91  			}
    92  		case <-scraper.foreignScrapper.shutdown: // user requested shutdown
    93  			log.Printf("CoinMarketCapscraper shutting down")
    94  			return
    95  		}
    96  	}
    97  
    98  }
    99  
   100  // Update retrieves new coin information from the CoinMarketCap API and stores it to influx
   101  func (scraper *CoinMarketCapScraper) UpdateQuotation() error {
   102  	log.Printf("Executing CoinMarketCapScraper update")
   103  
   104  	listing, err := getCoinMarketCapData()
   105  
   106  	if err != nil {
   107  		log.Errorln("Error getting data from CoinMarketCap", err)
   108  		return err
   109  	}
   110  
   111  	for _, coin := range listing.Data {
   112  
   113  		// TO DO: normalize symbol instead of all upper
   114  		coin.Symbol = strings.ToUpper(coin.Symbol)
   115  
   116  		// Parse last updated timestamp
   117  		layout := "2006-01-02T15:04:05.000Z"
   118  		timestamp, err := time.Parse(layout, coin.LastUpdated)
   119  		if err != nil {
   120  			log.Errorln("error parsing time")
   121  		}
   122  
   123  		// Get yesterday's price from influx if available
   124  		priceYesterday, err := scraper.foreignScrapper.datastore.GetForeignPriceYesterday(coin.Symbol, source)
   125  		if err != nil {
   126  			priceYesterday = 0
   127  		}
   128  		usdQuote, ok := coin.Quote["USD"]
   129  		if !ok {
   130  			log.Warnf("Couldn't find usd price for coin: %v", coin.Symbol)
   131  			continue
   132  		}
   133  		foreignQuotation := models.ForeignQuotation{
   134  			Symbol:             coin.Symbol,
   135  			Name:               coin.Name,
   136  			Price:              usdQuote.Price,
   137  			PriceYesterday:     priceYesterday,
   138  			VolumeYesterdayUSD: usdQuote.Volume24H,
   139  			Source:             coinMarketCapsource,
   140  			Time:               timestamp,
   141  		}
   142  		scraper.foreignScrapper.chanQuotation <- &foreignQuotation
   143  	}
   144  	return nil
   145  
   146  }
   147  
   148  func (scraper *CoinMarketCapScraper) GetQuoteChannel() chan *models.ForeignQuotation {
   149  	return scraper.foreignScrapper.chanQuotation
   150  }
   151  
   152  func getApiKey() string {
   153  	if utils.Getenv("USE_ENV", "false") == "true" {
   154  		return utils.Getenv("CMC_API_KEY", "")
   155  	}
   156  	var lines []string
   157  	file, err := os.Open("/run/secrets/Coinmarketcap-API.key") // Read in key information
   158  	if err != nil {
   159  		log.Fatal(err)
   160  	}
   161  	defer func() {
   162  		cerr := file.Close()
   163  		if err == nil {
   164  			err = cerr
   165  		}
   166  	}()
   167  
   168  	scanner := bufio.NewScanner(file)
   169  	for scanner.Scan() {
   170  		lines = append(lines, scanner.Text())
   171  	}
   172  	if err = scanner.Err(); err != nil {
   173  		log.Fatal(err)
   174  	}
   175  	if len(lines) != 1 {
   176  		log.Fatal("Secrets file for coinmarketcap API key should have exactly one line")
   177  	}
   178  	return lines[0]
   179  }
   180  
   181  func getCoinMarketCapData() (listing CoinMarketCapListing, err error) {
   182  	// There must be a pro coinmarketcap api key for this to work properly
   183  	apiKey := getApiKey()
   184  
   185  	req, err := http.NewRequestWithContext(context.Background(), "GET", "https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest", nil)
   186  	if err != nil {
   187  		log.Print(err)
   188  		return
   189  	}
   190  
   191  	q := url.Values{}
   192  	q.Add("start", "1")
   193  	q.Add("limit", "200")
   194  	q.Add("convert", "USD")
   195  	q.Add("sort", "market_cap")
   196  	q.Add("sort_dir", "desc")
   197  
   198  	req.Header.Set("Accepts", "application/json")
   199  	req.Header.Add("X-CMC_PRO_API_KEY", apiKey)
   200  	req.URL.RawQuery = q.Encode()
   201  
   202  	response, statusCode, err := utils.HTTPRequest(req)
   203  	if err != nil {
   204  		fmt.Println("Error sending request to server")
   205  		os.Exit(1)
   206  	}
   207  	fmt.Println(statusCode)
   208  
   209  	err = json.Unmarshal(response, &listing)
   210  	if err != nil {
   211  		return
   212  	}
   213  	return
   214  }
   215  
   216  /*
   217  // closes all connected Scrapers. Must only be called from mainLoop
   218  func (scraper *CoinMarketCapScraper) cleanup(err error) {
   219  
   220  	scraper.foreignScrapper.errorLock.Lock()
   221  	defer scraper.foreignScrapper.errorLock.Unlock()
   222  
   223  	scraper.foreignScrapper.tickerRate.Stop()
   224  	scraper.foreignScrapper.tickerState.Stop()
   225  
   226  	if err != nil {
   227  		scraper.foreignScrapper.error = err
   228  	}
   229  	scraper.foreignScrapper.closed = true
   230  
   231  	close(scraper.foreignScrapper.shutdownDone) // signal that shutdown is complete
   232  }
   233  */
   234  // Close closes any existing API connections
   235  func (scraper *CoinMarketCapScraper) Close() error {
   236  	if scraper.foreignScrapper.closed {
   237  		return errors.New("scraper already closed")
   238  	}
   239  	close(scraper.foreignScrapper.shutdown)
   240  	<-scraper.foreignScrapper.shutdownDone
   241  	scraper.foreignScrapper.errorLock.RLock()
   242  	defer scraper.foreignScrapper.errorLock.RUnlock()
   243  	return scraper.foreignScrapper.error
   244  }