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  }