Skip to content

Commit

Permalink
API endpoint for creating DAO coin market orders (deso-protocol#333)
Browse files Browse the repository at this point in the history
* Add support for DAO coin market orders

* Cleanup

* Improve comments and spacing

* Add error message for unsupported GOOD_TILL_CANCELLED fill type

* Address Nina's comments

Co-authored-by: Lazy Nina <[email protected]>
  • Loading branch information
tholonious and lazynina authored Apr 26, 2022
1 parent c9f0874 commit 5c318e8
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 55 deletions.
22 changes: 22 additions & 0 deletions routes/dao_coin_exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,25 @@ func orderOperationTypeToUint64(
}
return 0, errors.Errorf("Unknown string value for DAOCoinLimitOrderOperationType %v", operationType)
}

type DAOCoinLimitOrderFillTypeString string

const (
DAOCoinLimitOrderFillTypeGoodTillCancelled DAOCoinLimitOrderFillTypeString = "GOOD_TILL_CANCELLED"
DAOCoinLimitOrderFillTypeFillOrKill DAOCoinLimitOrderFillTypeString = "FILL_OR_KILL"
DAOCoinLimitOrderFillTypeImmediateOrCancel DAOCoinLimitOrderFillTypeString = "IMMEDIATE_OR_CANCEL"
)

func orderFillTypeToUint64(
fillType DAOCoinLimitOrderFillTypeString,
) (lib.DAOCoinLimitOrderFillType, error) {
switch fillType {
case DAOCoinLimitOrderFillTypeGoodTillCancelled:
return lib.DAOCoinLimitOrderFillTypeGoodTillCancelled, nil
case DAOCoinLimitOrderFillTypeFillOrKill:
return lib.DAOCoinLimitOrderFillTypeFillOrKill, nil
case DAOCoinLimitOrderFillTypeImmediateOrCancel:
return lib.DAOCoinLimitOrderFillTypeImmediateOrCancel, nil
}
return 0, errors.Errorf("Unknown DAO coin limit order fill type %v", fillType)
}
8 changes: 8 additions & 0 deletions routes/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const (
RoutePathDAOCoin = "/api/v0/dao-coin"
RoutePathTransferDAOCoin = "/api/v0/transfer-dao-coin"
RoutePathCreateDAOCoinLimitOrder = "/api/v0/create-dao-coin-limit-order"
RoutePathCreateDAOCoinMarketOrder = "/api/v0/create-dao-coin-market-order"
RoutePathCancelDAOCoinLimitOrder = "/api/v0/cancel-dao-coin-limit-order"
RoutePathAppendExtraData = "/api/v0/append-extra-data"
RoutePathGetTransactionSpending = "/api/v0/get-transaction-spending"
Expand Down Expand Up @@ -886,6 +887,13 @@ func (fes *APIServer) NewRouter() *muxtrace.Router {
fes.CreateDAOCoinLimitOrder,
PublicAccess,
},
{
"CreateDAOCoinMarketOrder",
[]string{"POST", "OPTIONS"},
RoutePathCreateDAOCoinMarketOrder,
fes.CreateDAOCoinMarketOrder,
PublicAccess,
},
{
"CancelDAOCoinLimitOrder",
[]string{"POST", "OPTIONS"},
Expand Down
225 changes: 170 additions & 55 deletions routes/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -2588,23 +2588,9 @@ func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http.
return
}

// An empty string for a buying or selling coin represents $DESO. At least of the coins must be a DAO coin however
if requestData.SellingDAOCoinCreatorPublicKeyBase58CheckOrUsername == "" &&
requestData.BuyingDAOCoinCreatorPublicKeyBase58CheckOrUsername == "" {
_AddBadRequestError(
ww,
"CreateDAOCoinLimitOrder: must provide at least one of BuyingDAOCoinCreatorPublicKeyBase58CheckOrUsername "+
"or SellingDAOCoinCreatorPublicKeyBase58CheckOrUsername",
)
return
}

// Basic validation that we have a transactor
if requestData.TransactorPublicKeyBase58Check == "" {
_AddBadRequestError(
ww,
"CreateDAOCoinLimitOrder: must provide a TransactorPublicKeyBase58Check",
)
_AddBadRequestError(ww, "CreateDAOCoinLimitOrder: must provide a TransactorPublicKeyBase58Check")
return
}

Expand Down Expand Up @@ -2640,10 +2626,7 @@ func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http.
// Validate operation type
operationType, err := orderOperationTypeToUint64(requestData.OperationType)
if err != nil {
_AddBadRequestError(
ww,
fmt.Sprintf("CreateDAOCoinLimitOrder: invalid OperationType %v", requestData.OperationType),
)
_AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err))
return
}

Expand All @@ -2654,66 +2637,196 @@ func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http.
}

// Decode and validate the buying / selling coin public keys
buyingCoinPublicKey := lib.ZeroPublicKey.ToBytes()
sellingCoinPublicKey := lib.ZeroPublicKey.ToBytes()
buyingCoinPublicKey, sellingCoinPublicKey, err := fes.getBuyingAndSellingDAOCoinPublicKeys(
utxoView,
requestData.BuyingDAOCoinCreatorPublicKeyBase58CheckOrUsername,
requestData.SellingDAOCoinCreatorPublicKeyBase58CheckOrUsername,
)
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err))
return
}

if requestData.BuyingDAOCoinCreatorPublicKeyBase58CheckOrUsername != "" {
buyingCoinPublicKey, _, err = fes.GetPubKeyAndProfileEntryForUsernameOrPublicKeyBase58Check(
requestData.BuyingDAOCoinCreatorPublicKeyBase58CheckOrUsername,
utxoView,
)
if err != nil {
_AddBadRequestError(
ww,
fmt.Sprintf(
"CreateDAOCoinLimitOrder: Error getting public key for %v: %v",
requestData.BuyingDAOCoinCreatorPublicKeyBase58CheckOrUsername,
err,
),
)
}
res, err := fes.createDAOCoinLimitOrderResponse(
utxoView,
requestData.TransactorPublicKeyBase58Check,
buyingCoinPublicKey,
sellingCoinPublicKey,
scaledExchangeRateCoinsToSellPerCoinToBuy,
quantityToFillInBaseUnits,
operationType,
lib.DAOCoinLimitOrderFillTypeGoodTillCancelled,
nil,
requestData.MinFeeRateNanosPerKB,
requestData.TransactionFees,
)
if err != nil {
_AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err))
return
}

if requestData.SellingDAOCoinCreatorPublicKeyBase58CheckOrUsername != "" {
sellingCoinPublicKey, _, err = fes.GetPubKeyAndProfileEntryForUsernameOrPublicKeyBase58Check(
requestData.SellingDAOCoinCreatorPublicKeyBase58CheckOrUsername,
utxoView,
if err = json.NewEncoder(ww).Encode(res); err != nil {
_AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: Problem encoding response as JSON: %v", err))
return
}
}

type DAOCoinMarketOrderWithQuantityRequest struct {
// The public key of the user who is sending the order
TransactorPublicKeyBase58Check string `safeForLogging:"true"`

// The public key or profile username of the DAO coin being bought
BuyingDAOCoinCreatorPublicKeyBase58CheckOrUsername string `safeForLogging:"true"`

// The public key or profile username of the DAO coin being sold
SellingDAOCoinCreatorPublicKeyBase58CheckOrUsername string `safeForLogging:"true"`

QuantityToFill float64 `safeForLogging:"true"`

OperationType DAOCoinLimitOrderOperationTypeString `safeForLogging:"true"`
FillType DAOCoinLimitOrderFillTypeString `safeForLogging:"true"`

MinFeeRateNanosPerKB uint64 `safeForLogging:"true"`
TransactionFees []TransactionFee `safeForLogging:"true"`
}

func (fes *APIServer) CreateDAOCoinMarketOrder(ww http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes))
requestData := DAOCoinMarketOrderWithQuantityRequest{}

if err := decoder.Decode(&requestData); err != nil {
_AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: Problem parsing request body: %v", err))
return
}

// Basic validation that we have a transactor
if requestData.TransactorPublicKeyBase58Check == "" {
_AddBadRequestError(ww, "CreateDAOCoinMarketOrder: must provide a TransactorPublicKeyBase58Check")
return
}

// Validate and convert quantity to base units
if requestData.QuantityToFill <= 0 {
_AddBadRequestError(ww, fmt.Sprint("CreateDAOCoinMarketOrder: QuantityToFill must be greater than 0"))
return
}
quantityToFillInBaseUnits, err := calculateQuantityToFillAsBaseUnits(
requestData.QuantityToFill,
)
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err))
return
}

// Validate operation type
operationType, err := orderOperationTypeToUint64(requestData.OperationType)
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err))
return
}

// Validate fill type
fillType, err := orderFillTypeToUint64(requestData.FillType)
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err))
return
}
if fillType == lib.DAOCoinLimitOrderFillTypeGoodTillCancelled {
_AddBadRequestError(
ww,
fmt.Sprintf("CreateDAOCoinMarketOrder: %v fill type not supported for market orders", requestData.FillType),
)
if err != nil {
_AddBadRequestError(
ww,
fmt.Sprintf(
"CreateDAOCoinLimitOrder: Error getting public key for %v: %v",
requestData.SellingDAOCoinCreatorPublicKeyBase58CheckOrUsername,
err,
),
)
}
return
}

utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView()
if err != nil {
_AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: problem fetching utxoView: %v", err))
return
}

// Decode and validate the buying / selling coin public keys
buyingCoinPublicKey, sellingCoinPublicKey, err := fes.getBuyingAndSellingDAOCoinPublicKeys(
utxoView,
requestData.BuyingDAOCoinCreatorPublicKeyBase58CheckOrUsername,
requestData.SellingDAOCoinCreatorPublicKeyBase58CheckOrUsername,
)
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err))
return
}

// override the initial value and explicitly set to 0 for clarity
zeroUint256 := uint256.NewInt().SetUint64(0)

res, err := fes.createDAOCoinLimitOrderResponse(
utxoView,
requestData.TransactorPublicKeyBase58Check,
buyingCoinPublicKey,
sellingCoinPublicKey,
scaledExchangeRateCoinsToSellPerCoinToBuy,
zeroUint256,
quantityToFillInBaseUnits,
operationType,
fillType,
nil,
requestData.MinFeeRateNanosPerKB,
requestData.TransactionFees,
)
if err != nil {
_AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err))
_AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err))
return
}

if err = json.NewEncoder(ww).Encode(res); err != nil {
_AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: Problem encoding response as JSON: %v", err))
_AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: Problem encoding response as JSON: %v", err))
return
}
}

// getBuyingAndSellingDAOCoinPublicKeys
// An empty string for the buying or selling coin represents $DESO. This enables $DESO <> DAO coin trades, and
// DAO coin <> DAO coin trades. At most one of the buying or selling coin can specify $DESO as we don't enable
// $DESO <> $DESO trades
func (fes *APIServer) getBuyingAndSellingDAOCoinPublicKeys(
utxoView *lib.UtxoView,
buyingDAOCoinCreatorPublicKeyBase58CheckOrUsername string,
sellingDAOCoinCreatorPublicKeyBase58CheckOrUsername string,
) ([]byte, []byte, error) {
if sellingDAOCoinCreatorPublicKeyBase58CheckOrUsername == "" &&
buyingDAOCoinCreatorPublicKeyBase58CheckOrUsername == "" {
return nil, nil, errors.Errorf("empty string provided for both the " +
"coin to buy and the coin to sell. At least one must specify a valid DAO public key or username whose coin " +
"will be bought or sold")
}

buyingCoinPublicKey := lib.ZeroPublicKey.ToBytes()
sellingCoinPublicKey := lib.ZeroPublicKey.ToBytes()

var err error

if buyingDAOCoinCreatorPublicKeyBase58CheckOrUsername != "" {
buyingCoinPublicKey, _, err = fes.GetPubKeyAndProfileEntryForUsernameOrPublicKeyBase58Check(
buyingDAOCoinCreatorPublicKeyBase58CheckOrUsername,
utxoView,
)
if err != nil {
return nil, nil, err
}
}

if sellingDAOCoinCreatorPublicKeyBase58CheckOrUsername != "" {
sellingCoinPublicKey, _, err = fes.GetPubKeyAndProfileEntryForUsernameOrPublicKeyBase58Check(
sellingDAOCoinCreatorPublicKeyBase58CheckOrUsername,
utxoView,
)
if err != nil {
return nil, nil, err
}
}

return buyingCoinPublicKey, sellingCoinPublicKey, nil
}

type DAOCoinLimitOrderWithCancelOrderIDRequest struct {
// The public key of the user who is cancelling the order
TransactorPublicKeyBase58Check string `safeForLogging:"true"`
Expand Down Expand Up @@ -2769,6 +2882,7 @@ func (fes *APIServer) CancelDAOCoinLimitOrder(ww http.ResponseWriter, req *http.
nil,
nil,
0,
0,
cancelOrderID,
requestData.MinFeeRateNanosPerKB,
requestData.TransactionFees,
Expand All @@ -2793,6 +2907,7 @@ func (fes *APIServer) createDAOCoinLimitOrderResponse(
scaledExchangeRateCoinsToSellPerCoinToBuy *uint256.Int,
quantityToFillInBaseUnits *uint256.Int,
operationType lib.DAOCoinLimitOrderOperationType,
fillType lib.DAOCoinLimitOrderFillType,
cancelOrderId *lib.BlockHash,
minFeeRateNanosPerKB uint64,
transactionFees []TransactionFee,
Expand Down Expand Up @@ -2824,6 +2939,7 @@ func (fes *APIServer) createDAOCoinLimitOrderResponse(
ScaledExchangeRateCoinsToSellPerCoinToBuy: scaledExchangeRateCoinsToSellPerCoinToBuy,
QuantityToFillInBaseUnits: quantityToFillInBaseUnits,
OperationType: operationType,
FillType: fillType,
CancelOrderID: cancelOrderId,
},
minFeeRateNanosPerKB,
Expand Down Expand Up @@ -3258,8 +3374,7 @@ func (fes *APIServer) TransactionSpendingLimitFromResponse(

if len(transactionSpendingLimitResponse.DAOCoinLimitOrderLimitMap) > 0 {
transactionSpendingLimit.DAOCoinLimitOrderLimitMap = make(map[lib.DAOCoinLimitOrderLimitKey]uint64)
for buyingPublicKey, sellingPublicKeyToCountMap :=
range transactionSpendingLimitResponse.DAOCoinLimitOrderLimitMap {
for buyingPublicKey, sellingPublicKeyToCountMap := range transactionSpendingLimitResponse.DAOCoinLimitOrderLimitMap {
buyingPKID := &lib.ZeroPKID
if buyingPublicKey != DAOCoinLimitOrderDESOPublicKey {
buyingPKID, err = getCreatorPKIDForBase58Check(buyingPublicKey)
Expand Down

0 comments on commit 5c318e8

Please sign in to comment.