Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add gate and deso dex for deso price #692

Merged
merged 2 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 126 additions & 8 deletions routes/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ type GetExchangeRateResponse struct {
USDCentsPerDeSoReserveExchangeRate uint64
BuyDeSoFeeBasisPoints uint64
USDCentsPerDeSoBlockchainDotCom uint64
USDCentsPerDeSoCoinbase uint64
USDCentsPerDeSoCoinbase uint64 // Deprecated

SatoshisPerBitCloutExchangeRate uint64 // Deprecated
USDCentsPerBitCloutExchangeRate uint64 // Deprecated
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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...")
Expand All @@ -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,
})

Expand Down
138 changes: 86 additions & 52 deletions routes/dao_coin_exchange_with_fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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 {
Expand All @@ -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),
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion routes/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion routes/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Loading