Skip to content

Commit

Permalink
add gate and deso dex for deso price (#692)
Browse files Browse the repository at this point in the history
* add gate for deso price

* use pricing from dex (#693)

* use pricing from dex

* multiply mid price usd by 100 to get cents

* Ln/update quote currency endpoint for deso (#694)

* update quote currency price in usd endpoint to use dex for price of deso

* deprecate fields and fill deprecated fields with deso dex price

* if most recent dex price is 0, return gate price

* add const for dusdc profile username
  • Loading branch information
lazynina authored Nov 4, 2024
1 parent 8a2f909 commit 3e2c00a
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 62 deletions.
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

0 comments on commit 3e2c00a

Please sign in to comment.