Skip to content

Commit

Permalink
Conversion of DAO coin limit order exchange rates and quantities from…
Browse files Browse the repository at this point in the history
… float64 <-> uint256 (deso-protocol#338)

* Change scaling factor to 1e18

* Proper handling for dao coin limit order exchange rates and quantities

* Fix typos and tighten up formatFloatAsString logic

* comments

* Address nits
  • Loading branch information
tholonious authored May 3, 2022
1 parent b007400 commit dc5df9e
Show file tree
Hide file tree
Showing 3 changed files with 547 additions and 70 deletions.
312 changes: 259 additions & 53 deletions routes/dao_coin_exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import (
"encoding/json"
"fmt"
"github.com/deso-protocol/core/lib"
"github.com/golang/glog"
"github.com/holiman/uint256"
"github.com/pkg/errors"
"io"
"math/big"
"net/http"
"strconv"
"strings"
)

type GetDAOCoinLimitOrdersRequest struct {
Expand Down Expand Up @@ -205,23 +208,17 @@ func (fes *APIServer) buildDAOCoinLimitOrderResponsesFromEntriesForCoinPair(
for _, order := range orders {
transactorPublicKey := utxoView.GetPublicKeyForPKID(order.TransactorPKID)

operationType, err := orderOperationTypeToString(order.OperationType)
if err != nil {
// It should not be possible to hit this error. If we do hit it, it means an order with an unsupported
// operation type made it through all validations during order creation, and was placed on the book. For
// these read-only API endpoints, we just skip the bad order and return all the valid orders we know of
continue
}

response := buildDAOCoinLimitOrderResponse(
response, err := buildDAOCoinLimitOrderResponse(
lib.Base58CheckEncode(transactorPublicKey, false, fes.Params),
buyingCoinPublicKeyBase58Check,
sellingCoinPublicKeyBase58Check,
operationType,
order,
)
if err != nil {
continue
}

responses = append(responses, response)
responses = append(responses, *response)
}

return responses
Expand All @@ -235,23 +232,24 @@ func (fes *APIServer) buildDAOCoinLimitOrderResponsesForTransactor(
var responses []DAOCoinLimitOrderEntryResponse

for _, order := range orders {
operationType, err := orderOperationTypeToString(order.OperationType)
if err != nil {
continue
}

buyingCoinPublicKeyBase58Check := fes.getPublicKeyBase58CheckForPKID(utxoView, order.BuyingDAOCoinCreatorPKID)
sellingCoinPublicKeyBase58Check := fes.getPublicKeyBase58CheckForPKID(utxoView, order.SellingDAOCoinCreatorPKID)

response := buildDAOCoinLimitOrderResponse(
response, err := buildDAOCoinLimitOrderResponse(
transactorPublicKeyBase58Check,
buyingCoinPublicKeyBase58Check,
sellingCoinPublicKeyBase58Check,
operationType,
order,
)
if err != nil {
glog.Errorf(
"buildDAOCoinLimitOrderResponsesForTransactor: Unable to build DAO coin limit order response for limit order with OrderID: %v",
order.OrderID,
)
continue
}

responses = append(responses, response)
responses = append(responses, *response)
}

return responses
Expand All @@ -269,61 +267,223 @@ func buildDAOCoinLimitOrderResponse(
transactorPublicKeyBase58Check string,
buyingCoinPublicKeyBase58Check string,
sellingCoinPublicKeyBase58Check string,
operationType DAOCoinLimitOrderOperationTypeString,
order *lib.DAOCoinLimitOrderEntry,
) DAOCoinLimitOrderEntryResponse {
return DAOCoinLimitOrderEntryResponse{
) (*DAOCoinLimitOrderEntryResponse, error) {
// It should not be possible to hit errors in this function. If we do hit them, it means an order with invalid
// values made it through all validations during order creation, and was placed on the book. In
// the read-only API endpoints, we just skip such bad orders and return all the valid orders we know of
operationTypeString, err := orderOperationTypeToString(order.OperationType)
if err != nil {
return nil, err
}

exchangeRate, err := CalculateExchangeRateAsFloat(
buyingCoinPublicKeyBase58Check,
sellingCoinPublicKeyBase58Check,
order.ScaledExchangeRateCoinsToSellPerCoinToBuy,
)
if err != nil {
return nil, err
}

quantityToFill, err := CalculateQuantityToFillAsFloat(
buyingCoinPublicKeyBase58Check,
sellingCoinPublicKeyBase58Check,
operationTypeString,
order.QuantityToFillInBaseUnits,
)
if err != nil {
return nil, err
}

return &DAOCoinLimitOrderEntryResponse{
TransactorPublicKeyBase58Check: transactorPublicKeyBase58Check,

BuyingDAOCoinCreatorPublicKeyBase58Check: buyingCoinPublicKeyBase58Check,
SellingDAOCoinCreatorPublicKeyBase58Check: sellingCoinPublicKeyBase58Check,
ExchangeRateCoinsToSellPerCoinToBuy: calculateFloatExchangeRate(
order.ScaledExchangeRateCoinsToSellPerCoinToBuy,
),
QuantityToFill: calculateQuantityToFillAsFloat(order.QuantityToFillInBaseUnits),
ExchangeRateCoinsToSellPerCoinToBuy: exchangeRate,
QuantityToFill: quantityToFill,

OperationType: operationType,
OperationType: operationTypeString,

OrderID: order.OrderID.String(),
}, nil
}

// CalculateScaledExchangeRate given a buying coin, selling coin, and a coin-level float exchange rate, this calculates
// the base unit to base unit exchange rate for the coin pair, while accounting for the difference in base unit scaling
// factors for $DESO (1e9) and DAO coins (1e18)
func CalculateScaledExchangeRate(
buyingCoinPublicKeyBase58CheckOrUsername string,
sellingCoinPublicKeyBase58CheckOrUsername string,
exchangeRateCoinsToSellPerCoinToBuy float64,
) (*uint256.Int, error) {
rawScaledExchangeRate, err := lib.CalculateScaledExchangeRateFromString(formatFloatAsString(exchangeRateCoinsToSellPerCoinToBuy))
if err != nil {
return nil, err
}
if rawScaledExchangeRate.IsZero() {
return nil, errors.Errorf("The float value %f is too small to produce a scaled exchange rate", exchangeRateCoinsToSellPerCoinToBuy)
}
if buyingCoinPublicKeyBase58CheckOrUsername == "" {
// Buying coin is $DESO
product := uint256.NewInt()
overflow := product.MulOverflow(rawScaledExchangeRate, getDESOToDAOCoinBaseUnitsScalingFactor())
if overflow {
return nil, errors.Errorf("Overflow when convering %f to a scaled exchange rate", exchangeRateCoinsToSellPerCoinToBuy)
}
return product, nil
} else if sellingCoinPublicKeyBase58CheckOrUsername == "" {
// Selling coin is $DESO
quotient := uint256.NewInt().Div(rawScaledExchangeRate, getDESOToDAOCoinBaseUnitsScalingFactor())
if quotient.IsZero() {
return nil, errors.Errorf("The float value %f is too small to produce a scaled exchange rate", exchangeRateCoinsToSellPerCoinToBuy)
}
return quotient, nil
}
return rawScaledExchangeRate, nil
}

// calculate (scaledValue / 10^38)
func calculateFloatExchangeRate(scaledValue *uint256.Int) float64 {
valueBigFloat := big.NewFloat(0).SetInt(scaledValue.ToBig())
divisorBigFloat := big.NewFloat(0).SetInt(lib.OneE38.ToBig())
// CalculateExchangeRateAsFloat given a buying coin, selling coin, and base unit to base unit exchange rate, this
// calculates the coin-level float exchange rate for the coin pair, while accounting for the difference in base unit
// scaling factors for $DESO (1e9) and DAO coins (1e18)
func CalculateExchangeRateAsFloat(
buyingCoinPublicKeyBase58CheckOrUsername string,
sellingCoinPublicKeyBase58CheckOrUsername string,
scaledValue *uint256.Int,
) (float64, error) {
scaledValueAsBigInt := scaledValue.ToBig()
if buyingCoinPublicKeyBase58CheckOrUsername == "" {
scaledValueAsBigInt.Div(scaledValueAsBigInt, getDESOToDAOCoinBaseUnitsScalingFactor().ToBig())
} else if sellingCoinPublicKeyBase58CheckOrUsername == "" {
scaledValueAsBigInt.Mul(scaledValueAsBigInt, getDESOToDAOCoinBaseUnitsScalingFactor().ToBig())
}

quotientBigFloat := big.NewFloat(0).Quo(valueBigFloat, divisorBigFloat)
oneE38AsBigInt := lib.OneE38.ToBig()

quotientFloat, _ := quotientBigFloat.Float64()
return quotientFloat
whole := big.NewInt(0).Div(scaledValueAsBigInt, oneE38AsBigInt)
decimal := big.NewInt(0).Mod(scaledValueAsBigInt, oneE38AsBigInt)
decimalLeadingZeros := strings.Repeat("0", getNumDigits(oneE38AsBigInt)-getNumDigits(decimal)-1)

str := fmt.Sprintf("%d.%s%d", whole, decimalLeadingZeros, decimal)
parsedFloat, err := strconv.ParseFloat(str, 64)
if err != nil {
// This should never happen since we're formatting the float ourselves above
return 0, err
}
return parsedFloat, nil
}

// calculate (quantityInBaseUnits * 10^9)
func calculateQuantityToFillAsFloat(quantityInBaseUnits *uint256.Int) float64 {
quantityInBaseUnitsAsBigFloat := big.NewFloat(0).SetInt(quantityInBaseUnits.ToBig())
divisor := big.NewFloat(float64(lib.NanosPerUnit))
quotientAsBigFloat := big.NewFloat(0).Quo(
quantityInBaseUnitsAsBigFloat,
divisor,
// CalculateQuantityToFillAsFloat given a buying coin, selling coin, operationType and a float quantity in base units,
// this calculates the float coin quantity for side the operationType refers to
func CalculateQuantityToFillAsFloat(
buyingCoinPublicKeyBase58CheckOrUsername string,
sellingCoinPublicKeyBase58CheckOrUsername string,
operationTypeString DAOCoinLimitOrderOperationTypeString,
quantityToFillInBaseUnits *uint256.Int,
) (float64, error) {
if isCoinToFillDESO(
buyingCoinPublicKeyBase58CheckOrUsername,
sellingCoinPublicKeyBase58CheckOrUsername,
operationTypeString,
) {
return calculateQuantityToFillFromDESONanosToFloat(quantityToFillInBaseUnits)
}
return calculateQuantityToFillFromDAOCoinBaseUnitsToFloat(quantityToFillInBaseUnits)
}

// calculate (quantityInBaseUnits / 10^18)
func calculateQuantityToFillFromDAOCoinBaseUnitsToFloat(quantityInBaseUnits *uint256.Int) (float64, error) {
return calculateQuantityToFillAsFloatWithScalingFactor(
quantityInBaseUnits,
lib.BaseUnitsPerCoin,
)
quotient, _ := quotientAsBigFloat.Float64()
return quotient
}

// calculate (quantityInBaseUnits / 10^9)
func calculateQuantityToFillAsBaseUnits(quantityToFill float64) (*uint256.Int, error) {
multiplier := big.NewFloat(float64(lib.NanosPerUnit))
product := big.NewFloat(0).Mul(
big.NewFloat(quantityToFill),
multiplier,
func calculateQuantityToFillFromDESONanosToFloat(quantityInNanos *uint256.Int) (float64, error) {
return calculateQuantityToFillAsFloatWithScalingFactor(
quantityInNanos,
uint256.NewInt().SetUint64(lib.NanosPerUnit),
)
productAsBigInt, _ := product.Int(nil)
productAsUint256, overflow := uint256.FromBig(productAsBigInt)
if overflow {
return nil, errors.Errorf("Overflow when converting quantity to buy from float to uint256")
}

// calculate (quantityInBaseUnits / 10^9)
func calculateQuantityToFillAsFloatWithScalingFactor(
quantityAsScaledValue *uint256.Int,
scalingFactor *uint256.Int,
) (float64, error) {
whole := uint256.NewInt().Div(quantityAsScaledValue, scalingFactor)
decimal := uint256.NewInt().Mod(quantityAsScaledValue, scalingFactor)
decimalLeadingZeros := strings.Repeat("0", getNumDigits(scalingFactor.ToBig())-getNumDigits(decimal.ToBig())-1)

str := fmt.Sprintf("%d.%s%d", whole, decimalLeadingZeros, decimal)
parsedFloat, err := strconv.ParseFloat(str, 64)
if err != nil {
// This should never happen since we're formatting the float ourselves above
return 0, err
}
return parsedFloat, nil
}

// CalculateQuantityToFillAsBaseUnits given a buying coin, selling coin, operationType and a float coin quantity,
// this calculates the quantity in base units for the side the operationType refers to
func CalculateQuantityToFillAsBaseUnits(
buyingCoinPublicKeyBase58CheckOrUsername string,
sellingCoinPublicKeyBase58CheckOrUsername string,
operationTypeString DAOCoinLimitOrderOperationTypeString,
quantityToFill float64,
) (*uint256.Int, error) {
if isCoinToFillDESO(
buyingCoinPublicKeyBase58CheckOrUsername,
sellingCoinPublicKeyBase58CheckOrUsername,
operationTypeString,
) {
return calculateQuantityToFillAsDESONanos(
quantityToFill,
)
}
return productAsUint256, nil
return calculateQuantityToFillAsDAOCoinBaseUnits(
quantityToFill,
)
}

// calculate (quantityToFill * 10^18)
func calculateQuantityToFillAsDAOCoinBaseUnits(quantityToFill float64) (*uint256.Int, error) {
return calculateQuantityToFillToBaseUnitsWithScalingFactor(
quantityToFill,
lib.BaseUnitsPerCoin,
)
}

// calculate (quantityToFill * 10^9)
func calculateQuantityToFillAsDESONanos(quantityToFill float64) (*uint256.Int, error) {
return calculateQuantityToFillToBaseUnitsWithScalingFactor(
quantityToFill,
uint256.NewInt().SetUint64(lib.NanosPerUnit),
)
}

// calculate (quantityToFill * scalingFactor)
func calculateQuantityToFillToBaseUnitsWithScalingFactor(
quantityToFill float64,
scalingFactor *uint256.Int,
) (*uint256.Int, error) {
return lib.ScaleFloatFormatStringToUint256(
formatFloatAsString(quantityToFill),
scalingFactor,
)
}

// given a buying coin, selling coin, and operation type, this determines if the QuantityToFill field
// for the coin the quantity field refers to is $DESO. If it's not $DESO, then it's assumed to be a DAO coin
func isCoinToFillDESO(
buyingCoinPublicKeyBase58CheckOrUsername string,
sellingCoinPublicKeyBase58CheckOrUsername string,
operationTypeString DAOCoinLimitOrderOperationTypeString,
) bool {
return buyingCoinPublicKeyBase58CheckOrUsername == "" && operationTypeString == DAOCoinLimitOrderOperationTypeStringBID ||
sellingCoinPublicKeyBase58CheckOrUsername == "" && operationTypeString == DAOCoinLimitOrderOperationTypeStringASK
}

// DAOCoinLimitOrderOperationTypeString A convenience type that uses a string to represent BID / ASK side in the API,
Expand Down Expand Up @@ -380,3 +540,49 @@ func orderFillTypeToUint64(
}
return 0, errors.Errorf("Unknown DAO coin limit order fill type %v", fillType)
}

// returns (1e18 / 1e9), which represents the difference in scaling factor for DAO coin base units and $DESO nanos
func getDESOToDAOCoinBaseUnitsScalingFactor() *uint256.Int {
return uint256.NewInt().Div(
lib.BaseUnitsPerCoin,
uint256.NewInt().SetUint64(lib.NanosPerUnit),
)
}

// 15 is a magic number that represents the precision supported by the IEEE-754 float64 standard.
//
// If f is large (1e15 or higher), then we truncate any values beyond the first 15 digits, as
// the lack of precision can introduce garbage when printing as string
//
// If f is small (ex: 1e-15), then we print up to 15 digits to the right of the decimal point
// to make sure we capture all digits within the supported precision, but without introducing garbage
//
// The range of supported values for f is [1e-15, 1e308] with precision for the 15 most significant digits. The
// minimum value for this range artificially set to 1e-15, but can be extended all the way 1e-308 with a bit better math
func formatFloatAsString(f float64) string {
fAsBigInt, _ := big.NewFloat(0).SetFloat64(f).Int(nil)
supportedPrecisionDigits := 15
numWholeNumberDigits := getNumDigits(fAsBigInt)
// f is small, we'll print up to 15 total digits to the right of the decimal point
if numWholeNumberDigits <= supportedPrecisionDigits {
return fmt.Sprintf("%."+fmt.Sprintf("%d", supportedPrecisionDigits-numWholeNumberDigits)+"f", f)
}
// f is a large number > 1e15, so we truncate any values after the first 15 digits
divisorToDropDigits := big.NewInt(10)
divisorToDropDigits.Exp(divisorToDropDigits, big.NewInt(int64(numWholeNumberDigits-supportedPrecisionDigits)), nil)
fAsBigInt.Div(fAsBigInt, divisorToDropDigits)
fAsBigInt.Mul(fAsBigInt, divisorToDropDigits)
return fmt.Sprintf("%d.0", fAsBigInt)
}

func getNumDigits(val *big.Int) int {
quotient := big.NewInt(0).Set(val)
zero := big.NewInt(0)
ten := big.NewInt(10)
numDigits := 0
for quotient.Cmp(zero) != 0 {
numDigits += 1
quotient.Div(quotient, ten)
}
return numDigits
}
Loading

0 comments on commit dc5df9e

Please sign in to comment.