github.com/decred/politeia@v1.4.0/politeiawww/legacy/prices.go (about) 1 // Copyright (c) 2019-2020 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package legacy 6 7 import ( 8 "context" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "math" 13 "net/http" 14 "strconv" 15 "time" 16 17 cms "github.com/decred/politeia/politeiawww/api/cms/v1" 18 www "github.com/decred/politeia/politeiawww/api/www/v1" 19 database "github.com/decred/politeia/politeiawww/legacy/cmsdatabase" 20 ) 21 22 const binanceURL = "https://api.binance.com" 23 const poloURL = "https://poloniex.com/public" 24 const httpTimeout = time.Second * 3 25 const pricePeriod = 900 26 27 const dcrSymbolBinance = "DCRBTC" 28 const usdtSymbolBinance = "BTCUSDT" 29 30 const dcrSymbolPolo = "BTC_DCR" 31 const usdtSymbolPolo = "USDT_BTC" 32 33 type poloChartData struct { 34 Date uint64 `json:"date"` 35 WeightedAverage float64 `json:"weightedAverage"` 36 } 37 38 // Set the last date to use polo as 4/1/2019. If any start date requested is 39 // after that date then use Binance instead. 40 var endPoloDate = time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC) 41 42 // getMonthAverage returns the average USD/DCR price for a given month 43 func getMonthAverage(ctx context.Context, month time.Month, year int) (uint, error) { 44 startTime := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC) 45 endTime := startTime.AddDate(0, 1, 0) 46 47 if time.Now().Before(endTime) { 48 return 0, fmt.Errorf( 49 "Requested rate end time (%v) is past current time (%v)", 50 endTime, 51 time.Now()) 52 } 53 54 unixStart := startTime.Unix() 55 unixEnd := endTime.Unix() 56 57 var dcrPrices map[uint64]float64 58 var btcPrices map[uint64]float64 59 var err error 60 61 // Use Binance if start date is AFTER 3/31/19 62 if startTime.Before(endPoloDate) { 63 // Download BTC/DCR and USDT/BTC prices from Polo 64 dcrPrices, err = getPricesPolo(ctx, dcrSymbolPolo, unixStart, unixEnd) 65 if err != nil { 66 return 0, fmt.Errorf("getPricesPolo %v: %v", dcrSymbolPolo, err) 67 } 68 btcPrices, err = getPricesPolo(ctx, usdtSymbolPolo, unixStart, unixEnd) 69 if err != nil { 70 return 0, fmt.Errorf("getPricesPolo %v: %v", usdtSymbolPolo, err) 71 } 72 } else { 73 // Download BTC/DCR and USDT/BTC prices from Binance 74 dcrPrices, err = getPricesBinance(ctx, dcrSymbolBinance, unixStart, unixEnd) 75 if err != nil { 76 return 0, fmt.Errorf("getPricesBinance %v: %v", dcrSymbolBinance, err) 77 } 78 btcPrices, err = getPricesBinance(ctx, usdtSymbolBinance, unixStart, unixEnd) 79 if err != nil { 80 return 0, fmt.Errorf("getPricesBinance %v: %v", usdtSymbolBinance, err) 81 } 82 } 83 // Create a map of unix timestamps => average price 84 usdtDcrPrices := make(map[uint64]float64) 85 86 // Select only timestamps which appear in both charts to 87 // populate the result set. Multiply BTC/DCR rate by 88 // USDT/BTC rate to get USDT/DCR rate. 89 for timestamp, dcr := range dcrPrices { 90 if btc, ok := btcPrices[timestamp]; ok { 91 usdtDcrPrices[timestamp] = dcr * btc 92 } 93 } 94 95 // Calculate and return the average of all USDT/DCR prices 96 var average float64 97 for _, price := range usdtDcrPrices { 98 average += price 99 } 100 average /= float64(len(usdtDcrPrices)) 101 102 return uint(math.Round(average * 100)), nil 103 } 104 105 // getPricesPolo contacts the Poloniex API to download 106 // price data for a given CC pairing. Returns a map 107 // of unix timestamp => average price 108 // Currently being replaced by Binance data due to Polo's ongoing issues with 109 // volume. 110 func getPricesPolo(ctx context.Context, pairing string, startDate int64, endDate int64) (map[uint64]float64, error) { 111 // Construct HTTP request and set parameters 112 req, err := http.NewRequestWithContext(ctx, http.MethodGet, poloURL, nil) 113 if err != nil { 114 return nil, err 115 } 116 117 q := req.URL.Query() 118 q.Set("command", "returnChartData") 119 q.Set("currencyPair", pairing) 120 q.Set("start", strconv.FormatInt(startDate, 10)) 121 q.Set("end", strconv.FormatInt(endDate, 10)) 122 q.Set("period", strconv.Itoa(pricePeriod)) 123 req.URL.RawQuery = q.Encode() 124 125 // Create HTTP client, 126 httpClient := http.Client{ 127 Timeout: httpTimeout, 128 } 129 130 // Send HTTP request 131 resp, err := httpClient.Do(req) 132 if err != nil { 133 return nil, err 134 } 135 136 defer resp.Body.Close() 137 138 // Read response and deserialise JSON 139 decoder := json.NewDecoder(resp.Body) 140 var chartData []poloChartData 141 err = decoder.Decode(&chartData) 142 if err != nil { 143 return nil, err 144 } 145 146 // Create a map of unix timestamps => average price 147 prices := make(map[uint64]float64, len(chartData)) 148 for _, data := range chartData { 149 prices[data.Date] = data.WeightedAverage 150 } 151 152 return prices, nil 153 } 154 155 // getPricesBinance contacts the Binance API to download 156 // price data for a given CC pairing. Returns a map 157 // of unix timestamp => average price 158 func getPricesBinance(ctx context.Context, pairing string, startDate int64, endDate int64) (map[uint64]float64, error) { 159 // Construct HTTP request and set parameters 160 req, err := http.NewRequestWithContext(ctx, http.MethodGet, binanceURL+"/api/v1/klines", nil) 161 if err != nil { 162 return nil, err 163 } 164 165 q := req.URL.Query() 166 q.Set("symbol", pairing) 167 q.Set("startTime", strconv.FormatInt(startDate*1000, 10)) 168 q.Set("endTime", strconv.FormatInt(endDate*1000, 10)) 169 170 // Request 1 hour intervals since there is a 1000 point limit on requests 171 // 31 Days * 24 Hours = 720 data points 172 q.Set("interval", "1h") 173 q.Set("limit", "1000") 174 175 req.URL.RawQuery = q.Encode() 176 177 // Create HTTP client, 178 httpClient := http.Client{ 179 Timeout: httpTimeout, 180 } 181 182 // Send HTTP request 183 resp, err := httpClient.Do(req) 184 if err != nil { 185 return nil, err 186 } 187 188 defer resp.Body.Close() 189 190 var chartData [][]json.RawMessage 191 192 // Read response and deserialise JSON into [][]json.RawMessage 193 decoder := json.NewDecoder(resp.Body) 194 err = decoder.Decode(&chartData) 195 if err != nil { 196 return nil, err 197 } 198 199 prices := make(map[uint64]float64, len(chartData)) 200 for _, v := range chartData { 201 var openTime uint64 // v[0] 202 var highStr string // v[2] 203 var lowStr string // v[3] 204 205 err := json.Unmarshal(v[0], &openTime) 206 if err != nil { 207 return nil, err 208 } 209 err = json.Unmarshal(v[2], &highStr) 210 if err != nil { 211 return nil, err 212 } 213 high, err := strconv.ParseFloat(highStr, 64) 214 if err != nil { 215 return nil, err 216 } 217 err = json.Unmarshal(v[3], &lowStr) 218 if err != nil { 219 return nil, err 220 } 221 low, err := strconv.ParseFloat(lowStr, 64) 222 if err != nil { 223 return nil, err 224 } 225 // Create a map of unix timestamps => average price 226 prices[openTime/1000] = (high + low) / 2 227 } 228 return prices, nil 229 } 230 231 // processInvoiceExchangeRate handles requests to return an exchange for a given 232 // month and year. It first attempts to find the exchange rate from the database 233 // and if none is found it requests the monthly average from the exchange API. 234 func (p *Politeiawww) processInvoiceExchangeRate(ctx context.Context, ier cms.InvoiceExchangeRate) (cms.InvoiceExchangeRateReply, error) { 235 reply := cms.InvoiceExchangeRateReply{} 236 237 monthAvg, err := p.cmsDB.ExchangeRate(int(ier.Month), int(ier.Year)) 238 if err != nil { 239 if errors.Is(err, database.ErrExchangeRateNotFound) { 240 monthAvgRaw, err := getMonthAverage(ctx, time.Month(ier.Month), int(ier.Year)) 241 if err != nil { 242 log.Errorf("processInvoiceExchangeRate: getMonthAverage: %v", err) 243 return reply, www.UserError{ 244 ErrorCode: cms.ErrorStatusInvalidExchangeRate, 245 } 246 } 247 248 monthAvg = &database.ExchangeRate{ 249 Month: ier.Month, 250 Year: ier.Year, 251 ExchangeRate: monthAvgRaw, 252 } 253 err = p.cmsDB.NewExchangeRate(monthAvg) 254 if err != nil { 255 return reply, err 256 } 257 } else { 258 return reply, err 259 } 260 } 261 reply.ExchangeRate = monthAvg.ExchangeRate 262 return reply, nil 263 }