diff --git a/routes/base.go b/routes/base.go index eeb6bcc9..f66a29b0 100644 --- a/routes/base.go +++ b/routes/base.go @@ -76,7 +76,7 @@ type GetExchangeRateResponse struct { USDCentsPerDeSoReserveExchangeRate uint64 BuyDeSoFeeBasisPoints uint64 USDCentsPerDeSoBlockchainDotCom uint64 - USDCentsPerDeSoCoinbase uint64 + USDCentsPerDeSoCoinbase uint64 // Deprecated SatoshisPerBitCloutExchangeRate uint64 // Deprecated USDCentsPerBitCloutExchangeRate uint64 // Deprecated @@ -130,10 +130,11 @@ func (fes *APIServer) GetExchangeRate(ww http.ResponseWriter, rr *http.Request) } func (fes *APIServer) GetExchangeDeSoPrice() uint64 { - if fes.UsdCentsPerDeSoExchangeRate > fes.USDCentsToDESOReserveExchangeRate { - return fes.UsdCentsPerDeSoExchangeRate + // We no longer observe a reserve rate. + if fes.MostRecentDesoDexPriceUSDCents == 0 { + return fes.MostRecentGatePriceUSDCents } - return fes.USDCentsToDESOReserveExchangeRate + return fes.MostRecentDesoDexPriceUSDCents } type BlockchainDeSoTickerResponse struct { @@ -247,6 +248,111 @@ func (fes *APIServer) GetCoinbaseExchangeRate() (_exchangeRate float64, _err err return usdCentsToDESOExchangePrice, nil } +type GateTickerResponse struct { + CurrencyPair string `json:"currency_pair"` + Last string `json:"last"` + LowestAsk string `json:"lowest_ask"` + LowestSize string `json:"lowest_size"` + HighestBid string `json:"highest_bid"` + HighestSize string `json:"highest_size"` + ChangePercentage string `json:"change_percentage"` + BaseVolume string `json:"base_volume"` + QuoteVolume string `json:"quote_volume"` + High24H string `json:"high_24h"` + Low24H string `json:"low_24h"` +} + +type currencyPair string + +const ( + GateDesoUsdt currencyPair = "deso_usdt" + GateUsdtUsd currencyPair = "usdt_usd" +) + +func getTickerResponseFromGate(currencyPair currencyPair) (*GateTickerResponse, error) { + httpClient := &http.Client{} + url := fmt.Sprintf("https://api.gateio.ws/api/v4/spot/tickers?currency_pair=%v", currencyPair) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + glog.Errorf("GetGateExchangeRate: Problem creating request: %v", err) + return nil, err + } + resp, err := httpClient.Do(req) + if err != nil { + glog.Errorf("GetGateExchangeRate: Problem making request: %v", err) + return nil, err + } + defer resp.Body.Close() + // Decode the response into the appropriate struct. + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + glog.Errorf("GetGateExchangeRate: Problem reading response body: %v", err) + return nil, err + } + responseData := []GateTickerResponse{} + decoder := json.NewDecoder(bytes.NewReader(body)) + if err = decoder.Decode(&responseData); err != nil { + glog.Errorf("GetGateExchangeRate: Problem decoding response JSON into "+ + "interface %v, response: %v, error: %v", responseData, resp, err) + return nil, err + } + if len(responseData) != 1 { + return nil, fmt.Errorf("GetGateExchangeRate: unexpected number of tickers returned from Gate: %v", len(responseData)) + } + return &responseData[0], nil +} + +func (fes *APIServer) GetGateExchangeRate() (_exchangeRate float64, _err error) { + desoToUSDTTickerResponse, err := getTickerResponseFromGate(GateDesoUsdt) + if err != nil { + glog.Errorf("GetGateExchangeRate: Problem fetching exchange rate from gate: %v", err) + return 0, err + } + usdtToUSDTickerResponse, err := getTickerResponseFromGate(GateUsdtUsd) + if err != nil { + glog.Errorf("GetGateExchangeRate: Problem fetching exchange rate from gate: %v", err) + return 0, err + } + usdtToUSDExchangePrice, err := strconv.ParseFloat(usdtToUSDTickerResponse.Last, 64) + if err != nil { + glog.Errorf("GetGateExchangeRate: Problem parsing USDT amount as float: %v", err) + return 0, err + } + desoToUSDTExchangePrice, err := strconv.ParseFloat(desoToUSDTTickerResponse.Last, 64) + if err != nil { + glog.Errorf("GetGateExchangeRate: Problem parsing DESO amount as float: %v", err) + return 0, err + } + + // usdCents/DESO = (usdt/USD) * (DESO/USDT) * 100 + usdCentsToDESOExchangePrice := (usdtToUSDExchangePrice * desoToUSDTExchangePrice) * 100 + if fes.backendServer != nil && fes.backendServer.GetStatsdClient() != nil { + if err = fes.backendServer.GetStatsdClient().Gauge("GATE_LAST_TRADE_PRICE", usdCentsToDESOExchangePrice, []string{}, 1); err != nil { + glog.Errorf("GetGateExchangeRate: Error logging Last Trade Price of %f to datadog: %v", usdCentsToDESOExchangePrice, err) + } + } + return usdCentsToDESOExchangePrice, nil +} + +func (fes *APIServer) GetExchangeRateFromDeSoDex() (float64, error) { + utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView() + if err != nil { + return 0, err + } + usdcProfileEntry := utxoView.GetProfileEntryForUsername([]byte(dusdcProfileUsername)) + if usdcProfileEntry == nil { + return 0, fmt.Errorf("GetExchangeRateFromDeSoDex: Could not find profile entry for dusdc_") + } + + usdcPKID := utxoView.GetPKIDForPublicKey(usdcProfileEntry.PublicKey) + + midPriceUSD, _, _, err := fes.GetHighestBidAndLowestAskPriceFromPKIDs(&lib.ZeroPKID, usdcPKID.PKID, utxoView, 0) + if err != nil { + return 0, err + } + return midPriceUSD * 100, nil +} + // UpdateUSDCentsToDeSoExchangeRate updates app state's USD Cents per DeSo value func (fes *APIServer) UpdateUSDCentsToDeSoExchangeRate() { glog.V(2).Info("Refreshing exchange rate...") @@ -265,17 +371,29 @@ func (fes *APIServer) UpdateUSDCentsToDeSoExchangeRate() { glog.Errorf("UpdateUSDCentsToDeSoExchangeRate: Error fetching exchange rate from coinbase: %v", err) } - // Take the max - lastTradePrice, err := stats.Max([]float64{blockchainDotComPrice, coinbasePrice}) + // Fetch price from gate + gatePrice, err := fes.GetGateExchangeRate() + glog.V(2).Infof("Gate price (USD Cents): %v", gatePrice) + if err != nil { + glog.Errorf("UpdateUSDCentsToDeSoExchangeRate: Error fetching exchange rate from gate: %v", err) + } + + desoDexPrice, err := fes.GetExchangeRateFromDeSoDex() + glog.V(2).Infof("DeSoDex price (USD Cents): %v", desoDexPrice) + if err != nil { + glog.Errorf("UpdateUSDCentsToDeSoExchangeRate: Error fetching exchange rate from DeSoDex: %v", err) + } // store the most recent exchange prices - fes.MostRecentCoinbasePriceUSDCents = uint64(coinbasePrice) + fes.MostRecentCoinbasePriceUSDCents = uint64(desoDexPrice) fes.MostRecentBlockchainDotComPriceUSDCents = uint64(blockchainDotComPrice) + fes.MostRecentGatePriceUSDCents = uint64(gatePrice) + fes.MostRecentDesoDexPriceUSDCents = uint64(desoDexPrice) // Get the current timestamp and append the current last trade price to the LastTradeDeSoPriceHistory slice timestamp := uint64(time.Now().UnixNano()) fes.LastTradeDeSoPriceHistory = append(fes.LastTradeDeSoPriceHistory, LastTradePriceHistoryItem{ - LastTradePrice: uint64(lastTradePrice), + LastTradePrice: uint64(desoDexPrice), Timestamp: timestamp, }) diff --git a/routes/dao_coin_exchange_with_fees.go b/routes/dao_coin_exchange_with_fees.go index 7e41b01e..2c8162a6 100644 --- a/routes/dao_coin_exchange_with_fees.go +++ b/routes/dao_coin_exchange_with_fees.go @@ -16,6 +16,10 @@ import ( "strings" ) +const ( + dusdcProfileUsername = "dusdc_" +) + type UpdateDaoCoinMarketFeesRequest struct { // The profile that the fees are being modified for. ProfilePublicKeyBase58Check string `safeForLogging:"true"` @@ -908,22 +912,31 @@ const FOCUS_FLOOR_PRICE_DESO_NANOS = 166666 func (fes *APIServer) GetQuoteCurrencyPriceInUsd( quoteCurrencyPublicKey string) (_midmarket string, _bid string, _ask string, _err error) { - if IsDesoPkid(quoteCurrencyPublicKey) { - // TODO: We're taking the Coinbase price directly here, but ideally we would get it from - // a function that abstracts away the exchange we're getting it from. We do this for now - // in order to minimize discrepancies with other sources. - desoUsdCents := fes.MostRecentCoinbasePriceUSDCents - if desoUsdCents == 0 { - return "", "", "", fmt.Errorf("GetQuoteCurrencyPriceInUsd: Coinbase DESO price is zero") - } - price := fmt.Sprintf("%0.9f", float64(desoUsdCents)/100) - return price, price, price, nil // TODO: get real bid and ask prices. - } utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView() if err != nil { return "", "", "", fmt.Errorf( "GetQuoteCurrencyPriceInUsd: Error fetching mempool view: %v", err) } + if IsDesoPkid(quoteCurrencyPublicKey) { + usdcProfileEntry := utxoView.GetProfileEntryForUsername([]byte(dusdcProfileUsername)) + if usdcProfileEntry == nil { + return "", "", "", fmt.Errorf("GetQuoteCurrencyPriceInUsd: Could not find profile entry for dusdc_") + } + + usdcPKID := utxoView.GetPKIDForPublicKey(usdcProfileEntry.PublicKey) + midMarketPrice, highestBidPrice, lowestAskPrice, err := fes.GetHighestBidAndLowestAskPriceFromPKIDs( + &lib.ZeroPKID, usdcPKID.PKID, utxoView, 0) + if err != nil { + return "", "", "", fmt.Errorf("GetQuoteCurrencyPriceInUsd: Error getting price for DESO: %v", err) + } + if highestBidPrice == 0.0 || lowestAskPrice == math.MaxFloat64 { + return "", "", "", fmt.Errorf("GetQuoteCurrencyPriceInUsd: Error calculating price for DESO") + } + return fmt.Sprintf("%0.9f", midMarketPrice), + fmt.Sprintf("%0.9f", highestBidPrice), + fmt.Sprintf("%0.9f", lowestAskPrice), + nil + } pkBytes, _, err := lib.Base58CheckDecode(quoteCurrencyPublicKey) if err != nil || len(pkBytes) != btcec.PubKeyBytesLenCompressed { @@ -942,66 +955,35 @@ func (fes *APIServer) GetQuoteCurrencyPriceInUsd( // If the profile is the dusdc profile then just return 1.0 lowerUsername := strings.ToLower(string(existingProfileEntry.Username)) - if lowerUsername == "dusdc_" { + if lowerUsername == dusdcProfileUsername { return "1.0", "1.0", "1.0", nil } else if lowerUsername == "focus" || lowerUsername == "openfund" { - // TODO: We're taking the Coinbase price directly here, but ideally we would get it from - // a function that abstracts away the exchange we're getting it from. We do this for now - // in order to minimize discrepancies with other sources. - desoUsdCents := fes.MostRecentCoinbasePriceUSDCents + // Get the exchange deso price. currently this function + // just returns the price from the deso dex. + desoUsdCents := fes.GetExchangeDeSoPrice() if desoUsdCents == 0 { return "", "", "", fmt.Errorf("GetQuoteCurrencyPriceInUsd: Coinbase DESO price is zero") } + pkid := utxoView.GetPKIDForPublicKey(pkBytes) if pkid == nil { return "", "", "", fmt.Errorf("GetQuoteCurrencyPriceInUsd: Error getting pkid for public key %v", quoteCurrencyPublicKey) } - ordersBuyingCoin1, err := utxoView.GetAllDAOCoinLimitOrdersForThisDAOCoinPair( - &lib.ZeroPKID, pkid.PKID) - if err != nil { - return "", "", "", fmt.Errorf("GetDAOCoinLimitOrders: Error getting limit orders: %v", err) - } - ordersBuyingCoin2, err := utxoView.GetAllDAOCoinLimitOrdersForThisDAOCoinPair( - pkid.PKID, &lib.ZeroPKID) - if err != nil { - return "", "", "", fmt.Errorf("GetDAOCoinLimitOrders: Error getting limit orders: %v", err) - } - allOrders := append(ordersBuyingCoin1, ordersBuyingCoin2...) // Find the highest bid price and the lowest ask price highestBidPrice := float64(0.0) if lowerUsername == "focus" { highestBidPrice = float64(FOCUS_FLOOR_PRICE_DESO_NANOS) / float64(lib.NanosPerUnit) } - lowestAskPrice := math.MaxFloat64 - for _, order := range allOrders { - priceStr, err := CalculatePriceStringFromScaledExchangeRate( - lib.PkToString(order.BuyingDAOCoinCreatorPKID[:], fes.Params), - lib.PkToString(order.SellingDAOCoinCreatorPKID[:], fes.Params), - order.ScaledExchangeRateCoinsToSellPerCoinToBuy, - DAOCoinLimitOrderOperationTypeString(order.OperationType.String())) - if err != nil { - return "", "", "", fmt.Errorf("GetQuoteCurrencyPriceInUsd: Error calculating price: %v", err) - } - priceFloat, err := strconv.ParseFloat(priceStr, 64) - if err != nil { - return "", "", "", fmt.Errorf("GetQuoteCurrencyPriceInUsd: Error parsing price: %v", err) - } - if order.OperationType == lib.DAOCoinLimitOrderOperationTypeBID && - priceFloat > highestBidPrice { - - highestBidPrice = priceFloat - } - if order.OperationType == lib.DAOCoinLimitOrderOperationTypeASK && - priceFloat < lowestAskPrice { - - lowestAskPrice = priceFloat - } + var lowestAskPrice, midPriceDeso float64 + midPriceDeso, highestBidPrice, lowestAskPrice, err = fes.GetHighestBidAndLowestAskPriceFromPKIDs( + pkid.PKID, &lib.ZeroPKID, utxoView, highestBidPrice) + if err != nil { + return "", "", "", fmt.Errorf("GetQuoteCurrencyPriceInUsd: Error getting price: %v", err) } if highestBidPrice != 0.0 && lowestAskPrice != math.MaxFloat64 { - midPriceDeso := (highestBidPrice + lowestAskPrice) / 2.0 midPriceUsd := midPriceDeso * float64(desoUsdCents) / 100 return fmt.Sprintf("%0.9f", midPriceUsd), @@ -1018,6 +1000,58 @@ func (fes *APIServer) GetQuoteCurrencyPriceInUsd( quoteCurrencyPublicKey) } +func (fes *APIServer) GetHighestBidAndLowestAskPriceFromPKIDs( + coin1PKID *lib.PKID, + coin2PKID *lib.PKID, + utxoView *lib.UtxoView, + initialHighestBidPrice float64, +) (float64, float64, float64, error) { + ordersBuyingCoin1, err := utxoView.GetAllDAOCoinLimitOrdersForThisDAOCoinPair( + coin1PKID, coin2PKID) + if err != nil { + return 0, 0, 0, fmt.Errorf("GetDAOCoinLimitOrders: Error getting limit orders: %v", err) + } + ordersBuyingCoin2, err := utxoView.GetAllDAOCoinLimitOrdersForThisDAOCoinPair( + coin2PKID, coin1PKID) + if err != nil { + return 0, 0, 0, fmt.Errorf("GetDAOCoinLimitOrders: Error getting limit orders: %v", err) + } + allOrders := append(ordersBuyingCoin1, ordersBuyingCoin2...) + // Find the highest bid price and the lowest ask price + highestBidPrice := initialHighestBidPrice + lowestAskPrice := math.MaxFloat64 + for _, order := range allOrders { + priceStr, err := CalculatePriceStringFromScaledExchangeRate( + lib.PkToString(order.BuyingDAOCoinCreatorPKID[:], fes.Params), + lib.PkToString(order.SellingDAOCoinCreatorPKID[:], fes.Params), + order.ScaledExchangeRateCoinsToSellPerCoinToBuy, + DAOCoinLimitOrderOperationTypeString(order.OperationType.String())) + if err != nil { + return 0, 0, 0, fmt.Errorf("GetQuoteCurrencyPriceInUsd: Error calculating price: %v", err) + } + priceFloat, err := strconv.ParseFloat(priceStr, 64) + if err != nil { + return 0, 0, 0, fmt.Errorf("GetQuoteCurrencyPriceInUsd: Error parsing price: %v", err) + } + if order.OperationType == lib.DAOCoinLimitOrderOperationTypeBID && + priceFloat > highestBidPrice { + + highestBidPrice = priceFloat + } + if order.OperationType == lib.DAOCoinLimitOrderOperationTypeASK && + priceFloat < lowestAskPrice { + + lowestAskPrice = priceFloat + } + } + if highestBidPrice != 0.0 && lowestAskPrice != math.MaxFloat64 { + midPrice := (highestBidPrice + lowestAskPrice) / 2.0 + + return midPrice, highestBidPrice, lowestAskPrice, nil + } + return 0, 0, 0, fmt.Errorf("GetQuoteCurrencyPriceInUsd: Error calculating price") +} + func (fes *APIServer) CreateMarketOrLimitOrder( isMarketOrder bool, request *DAOCoinLimitOrderCreationRequest, diff --git a/routes/server.go b/routes/server.go index e530b723..41430c81 100644 --- a/routes/server.go +++ b/routes/server.go @@ -398,8 +398,10 @@ type APIServer struct { LastTradePriceLookback uint64 // most recent exchange prices fetched - MostRecentCoinbasePriceUSDCents uint64 + MostRecentCoinbasePriceUSDCents uint64 // Deprecated MostRecentBlockchainDotComPriceUSDCents uint64 + MostRecentGatePriceUSDCents uint64 + MostRecentDesoDexPriceUSDCents uint64 // Base-58 prefix to check for to determine if a string could be a public key. PublicKeyBase58Prefix string diff --git a/routes/transaction.go b/routes/transaction.go index 61b94deb..41fb3e88 100644 --- a/routes/transaction.go +++ b/routes/transaction.go @@ -1199,7 +1199,7 @@ func (fes *APIServer) GetNanosFromUSDCents(usdCents float64, feeBasisPoints uint } func (fes *APIServer) GetUSDFromNanos(nanos uint64) float64 { - usdCentsPerDeSo := float64(fes.UsdCentsPerDeSoExchangeRate) + usdCentsPerDeSo := float64(fes.GetExchangeDeSoPrice()) return usdCentsPerDeSo * float64(nanos/lib.NanosPerUnit) / 100 }