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 }