From 5c318e874ec5772f7f62a7a1342c913711dc1124 Mon Sep 17 00:00:00 2001 From: iamsofonias <99746187+iamsofonias@users.noreply.github.com> Date: Tue, 26 Apr 2022 11:25:44 -0400 Subject: [PATCH] API endpoint for creating DAO coin market orders (#333) * 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 <81658138+lazynina@users.noreply.github.com> --- routes/dao_coin_exchange.go | 22 ++++ routes/server.go | 8 ++ routes/transaction.go | 225 +++++++++++++++++++++++++++--------- 3 files changed, 200 insertions(+), 55 deletions(-) diff --git a/routes/dao_coin_exchange.go b/routes/dao_coin_exchange.go index 6033ad45..4361b778 100644 --- a/routes/dao_coin_exchange.go +++ b/routes/dao_coin_exchange.go @@ -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) +} diff --git a/routes/server.go b/routes/server.go index 931edb60..4c32789c 100644 --- a/routes/server.go +++ b/routes/server.go @@ -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" @@ -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"}, diff --git a/routes/transaction.go b/routes/transaction.go index b13575a8..d0563489 100644 --- a/routes/transaction.go +++ b/routes/transaction.go @@ -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 } @@ -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 } @@ -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"` @@ -2769,6 +2882,7 @@ func (fes *APIServer) CancelDAOCoinLimitOrder(ww http.ResponseWriter, req *http. nil, nil, 0, + 0, cancelOrderID, requestData.MinFeeRateNanosPerKB, requestData.TransactionFees, @@ -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, @@ -2824,6 +2939,7 @@ func (fes *APIServer) createDAOCoinLimitOrderResponse( ScaledExchangeRateCoinsToSellPerCoinToBuy: scaledExchangeRateCoinsToSellPerCoinToBuy, QuantityToFillInBaseUnits: quantityToFillInBaseUnits, OperationType: operationType, + FillType: fillType, CancelOrderID: cancelOrderId, }, minFeeRateNanosPerKB, @@ -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)