diff --git a/go.mod b/go.mod index eb723781..c229ba50 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ replace github.com/deso-protocol/core => ../core/ require ( cloud.google.com/go/storage v1.27.0 github.com/btcsuite/btcd v0.21.0-beta - github.com/btcsuite/btcd/btcec/v2 v2.2.1 github.com/btcsuite/btcutil v1.0.2 github.com/davecgh/go-spew v1.1.1 github.com/deso-protocol/core v0.0.0-00010101000000-000000000000 @@ -45,6 +44,7 @@ require ( cloud.google.com/go/iam v0.8.0 // indirect github.com/DataDog/datadog-go v4.5.0+incompatible // indirect github.com/Microsoft/go-winio v0.4.16 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.2.1 // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/bwesterb/go-ristretto v1.2.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect diff --git a/routes/dao_coin_exchange.go b/routes/dao_coin_exchange.go index fe6486e8..e8f9dce2 100644 --- a/routes/dao_coin_exchange.go +++ b/routes/dao_coin_exchange.go @@ -869,6 +869,47 @@ func CalculateFloatQuantityFromBaseUnits( return calculateScaledUint256AsFloat(quantityToFillInBaseUnits.ToBig(), lib.BaseUnitsPerCoin.ToBig()) } +func CalculateBaseUnitsFromStringDecimalAmountSimple( + coinPkid string, + quantityToFill string, +) (*uint256.Int, error) { + // If we don't return zero here, we error later because it thinks we overflowed + quantityToFillFloat, err := strconv.ParseFloat(quantityToFill, 64) + if err != nil { + return nil, errors.Wrapf(err, "CalculateBaseUnitsFromStringDecimalAmountSimple: "+ + "Problem parsing quantity %v", quantityToFill) + } + if quantityToFillFloat == 0.0 { + return uint256.NewInt(), nil + } + if err := validateNonNegativeDecimalString(quantityToFill); err != nil { + return nil, err + } + + if IsDesoPkid(coinPkid) { + return calculateQuantityToFillAsDESONanos( + quantityToFill, + ) + } + return calculateQuantityToFillAsDAOCoinBaseUnits( + quantityToFill, + ) +} + +func CalculateStringDecimalAmountFromBaseUnitsSimple( + coinPkid string, + quantityToFillInBaseUnits *uint256.Int, +) (string, error) { + // If we don't return zero here, we error later because it thinks we overflowed + if quantityToFillInBaseUnits.IsZero() { + return "0.0", nil + } + if IsDesoPkid(coinPkid) { + return lib.FormatScaledUint256AsDecimalString(quantityToFillInBaseUnits.ToBig(), big.NewInt(int64(lib.NanosPerUnit))), nil + } + return lib.FormatScaledUint256AsDecimalString(quantityToFillInBaseUnits.ToBig(), lib.BaseUnitsPerCoin.ToBig()), 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( @@ -1175,7 +1216,8 @@ func (fes *APIServer) validateTransactorSellingCoinBalance( // Compare transactor selling balance to total selling quantity. if transactorSellingBalanceBaseUnits.Lt(totalSellingBaseUnits) { - return errors.Errorf("Insufficient balance to open order") + return errors.Errorf("Insufficient balance to open order: Need %v but have %v", + totalSellingBaseUnits, transactorSellingBalanceBaseUnits) } // Happy path. No error. Transactor has sufficient balance to cover their selling quantity. diff --git a/routes/dao_coin_exchange_with_fees.go b/routes/dao_coin_exchange_with_fees.go new file mode 100644 index 00000000..9ac8c110 --- /dev/null +++ b/routes/dao_coin_exchange_with_fees.go @@ -0,0 +1,1616 @@ +package routes + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "github.com/btcsuite/btcd/btcec" + "github.com/deso-protocol/core/lib" + "github.com/holiman/uint256" + "io" + "math" + "math/big" + "net/http" + "strconv" + "strings" +) + +type UpdateDaoCoinMarketFeesRequest struct { + // The public key of the user who is trying to update the + // fees for their market. + UpdaterPublicKeyBase58Check string `safeForLogging:"true"` + + // This is only set when the user wants to modify a profile + // that isn't theirs. Otherwise, the UpdaterPublicKeyBase58Check is + // assumed to own the profile being updated. + ProfilePublicKeyBase58Check string `safeForLogging:"true"` + + // A map of pubkey->feeBasisPoints that the user wants to set for their market. + // If the map contains {pk1: 100, pk2: 200} then the user is setting the + // feeBasisPoints for pk1 to 100 and the feeBasisPoints for pk2 to 200. + // This means that pk1 will get 1% of every taker's trade and pk2 will get + // 2%. + FeeBasisPointsByPublicKey map[string]uint64 `safeForLogging:"true"` + + MinFeeRateNanosPerKB uint64 `safeForLogging:"true"` + TransactionFees []TransactionFee `safeForLogging:"true"` + + OptionalPrecedingTransactions []*lib.MsgDeSoTxn `safeForLogging:"true"` +} + +type UpdateDaoCoinMarketFeesResponse struct { + TotalInputNanos uint64 + ChangeAmountNanos uint64 + FeeNanos uint64 + Transaction *lib.MsgDeSoTxn + TransactionHex string + TxnHashHex string +} + +func ValidateTradingFeeMap(feeMap map[string]uint64) error { + if len(feeMap) > 100 { + return fmt.Errorf("Trading fees map must have 100 or fewer entries") + } + for pkStr := range feeMap { + pkBytes, _, err := lib.Base58CheckDecode(pkStr) + if err != nil { + return fmt.Errorf("Trading fee map contains invalid public key: %v", pkStr) + } + if len(pkBytes) != btcec.PubKeyBytesLenCompressed { + return fmt.Errorf("Trading fee map contains invalid public key: %v", pkStr) + } + } + totalFeeBasisPoints := big.NewInt(0) + for _, feeBasisPoints := range feeMap { + if feeBasisPoints == 0 { + return fmt.Errorf("Trading fees must be greater than zero") + } + totalFeeBasisPoints.Add(totalFeeBasisPoints, big.NewInt(int64(feeBasisPoints))) + } + if totalFeeBasisPoints.Cmp(big.NewInt(100*100)) > 0 { + return fmt.Errorf("Trading fees must sum to less than 100 percent") + } + return nil +} + +func (fes *APIServer) UpdateDaoCoinMarketFees(ww http.ResponseWriter, req *http.Request) { + decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) + requestData := UpdateDaoCoinMarketFeesRequest{} + if err := decoder.Decode(&requestData); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateDaoCoinMarketFees: Problem parsing request body: %v", err)) + return + } + + // Decode the public key + updaterPublicKeyBytes, _, err := lib.Base58CheckDecode(requestData.UpdaterPublicKeyBase58Check) + if err != nil || len(updaterPublicKeyBytes) != btcec.PubKeyBytesLenCompressed { + _AddBadRequestError(ww, fmt.Sprintf( + "UpdateDAOCoinMarketFees: Problem decoding public key %s: %v", + requestData.UpdaterPublicKeyBase58Check, err)) + return + } + + // If we're missing trading fees then error + if len(requestData.FeeBasisPointsByPublicKey) == 0 { + _AddBadRequestError(ww, fmt.Sprintf("UpdateDaoCoinMarketFees: Must provide at least one fee to update")) + return + } + + // Validate the fee map. + if err := ValidateTradingFeeMap(requestData.FeeBasisPointsByPublicKey); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateDaoCoinMarketFees: %v", err)) + return + } + + utxoView, err := lib.GetAugmentedUniversalViewWithAdditionalTransactions( + fes.backendServer.GetMempool(), + requestData.OptionalPrecedingTransactions, + ) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateDaoCoinMarketFees: Error fetching mempool view: %v", err)) + return + } + + // When this is nil then the UpdaterPublicKey is assumed to be the owner of + // the profile. + var profilePublicKeyBytes []byte + if requestData.ProfilePublicKeyBase58Check != "" { + profilePublicKeyBytes, _, err = lib.Base58CheckDecode(requestData.ProfilePublicKeyBase58Check) + if err != nil || len(profilePublicKeyBytes) != btcec.PubKeyBytesLenCompressed { + _AddBadRequestError(ww, fmt.Sprintf( + "UpdateDaoCoinMarketFees: Problem decoding public key %s: %v", + requestData.ProfilePublicKeyBase58Check, err)) + return + } + } + + // Get the public key. + profilePublicKey := updaterPublicKeyBytes + if requestData.ProfilePublicKeyBase58Check != "" { + profilePublicKey = profilePublicKeyBytes + } + + // Pull the existing profile. If one doesn't exist, then we error. The user should + // create a profile first before trying to update the fee params for their market. + existingProfileEntry := utxoView.GetProfileEntryForPublicKey(profilePublicKey) + if existingProfileEntry == nil || existingProfileEntry.IsDeleted() { + _AddBadRequestError(ww, fmt.Sprintf( + "UpdateDaoCoinMarketFees: Profile for public key %v does not exist", + requestData.ProfilePublicKeyBase58Check)) + return + } + + // Update the fees on the just the trading fees on the extradata map of the profile. + feeMapByPkid := make(map[lib.PublicKey]uint64) + for pubkeyString, feeBasisPoints := range requestData.FeeBasisPointsByPublicKey { + pkBytes, _, err := lib.Base58CheckDecode(pubkeyString) + if err != nil || len(pkBytes) != btcec.PubKeyBytesLenCompressed { + _AddBadRequestError(ww, fmt.Sprintf( + "UpdateDaoCoinMarketFees: Problem decoding public key %s: %v", + pubkeyString, err)) + return + } + pkidEntry := utxoView.GetPKIDForPublicKey(pkBytes) + // TODO: Should maybe also check IsDeleted here, but it's impossible for it to be + // IsDeleted so it should be fine for now. + if pkidEntry == nil { + _AddBadRequestError(ww, fmt.Sprintf( + "UpdateDaoCoinMarketFees: PKID for public key %v does not exist", + pubkeyString)) + return + } + pkidBytes := pkidEntry.PKID[:] + if err != nil || len(pkidBytes) != btcec.PubKeyBytesLenCompressed { + _AddBadRequestError(ww, fmt.Sprintf( + "UpdateDaoCoinMarketFees: Problem decoding public key %s: %v", + pubkeyString, err)) + return + } + feeMapByPkid[*lib.NewPublicKey(pkidBytes)] = feeBasisPoints + } + feeMapByPkidBytes, err := lib.SerializePubKeyToUint64Map(feeMapByPkid) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateDaoCoinMarketFees: Problem serializing fee map: %v", err)) + return + } + // This will merge in with existing ExtraData. + additionalExtraData := make(map[string][]byte) + additionalExtraData[lib.TokenTradingFeesByPkidMapKey] = feeMapByPkidBytes + + // Compute the additional transaction fees as specified by the request body and the node-level fees. + additionalOutputs, err := fes.getTransactionFee( + lib.TxnTypeUpdateProfile, updaterPublicKeyBytes, requestData.TransactionFees) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("AuthorizeDerivedKey: TransactionFees specified in Request body are invalid: %v", err)) + return + } + + // Try and create the UpdateProfile txn for the user. + txn, totalInput, changeAmount, fees, err := fes.blockchain.CreateUpdateProfileTxn( + updaterPublicKeyBytes, + profilePublicKeyBytes, + "", // Don't update username + "", // Don't update description + "", // Don't update profile pic + existingProfileEntry.CreatorCoinEntry.CreatorBasisPoints, // Don't update creator basis points + // StakeMultipleBasisPoints is a deprecated field that we don't use anywhere and will delete soon. + // I noticed we use a hardcoded value of 12500 in the frontend and when creating a post so I'm doing + // the same here for now. + 1.25*100*100, // Don't update stake multiple basis points + existingProfileEntry.IsHidden, // Don't update hidden status + 0, // Don't add additionalFees + additionalExtraData, // The new ExtraData + requestData.MinFeeRateNanosPerKB, fes.backendServer.GetMempool(), + additionalOutputs) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateDaoCoinMarketFees: Problem creating transaction: %v", err)) + return + } + + txnBytes, err := txn.ToBytes(true) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateDaoCoinMarketFees: Problem serializing transaction: %v", err)) + return + } + + // Return all the data associated with the transaction in the response + res := UpdateDaoCoinMarketFeesResponse{ + TotalInputNanos: totalInput, + ChangeAmountNanos: changeAmount, + FeeNanos: fees, + Transaction: txn, + TransactionHex: hex.EncodeToString(txnBytes), + TxnHashHex: txn.Hash().String(), + } + if err = json.NewEncoder(ww).Encode(res); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateDaoCoinMarketFees: Problem encoding response as JSON: %v", err)) + return + } +} + +type GetDaoCoinMarketFeesRequest struct { + ProfilePublicKeyBase58Check string `safeForLogging:"true"` +} + +type GetDaoCoinMarketFeesResponse struct { + FeeBasisPointsByPublicKey map[string]uint64 `safeForLogging:"true"` +} + +func GetTradingFeesForMarket( + utxoView *lib.UtxoView, + params *lib.DeSoParams, + profilePublicKey string, +) ( + _feeMapByPubkey map[string]uint64, + _err error, +) { + + // Decode the public key + profilePublicKeyBytes, _, err := lib.Base58CheckDecode(profilePublicKey) + if err != nil || len(profilePublicKeyBytes) != btcec.PubKeyBytesLenCompressed { + return nil, fmt.Errorf( + "GetTradingFeesForMarket: Problem decoding public key %s: %v", + profilePublicKey, err) + } + + // Pull the existing profile. If one doesn't exist, then we error. The user should + // create a profile first before trying to update the fee params for their market. + existingProfileEntry := utxoView.GetProfileEntryForPublicKey(profilePublicKeyBytes) + if existingProfileEntry == nil || existingProfileEntry.IsDeleted() { + return nil, fmt.Errorf( + "GetTradingFeesForMarket: Profile for public key %v does not exist", + profilePublicKey) + } + + // Decode the trading fees from the profile. + tradingFeesByPkidBytes, exists := existingProfileEntry.ExtraData[lib.TokenTradingFeesByPkidMapKey] + tradingFeesMapPubkey := make(map[lib.PublicKey]uint64) + if exists { + tradingFeesMapByPkid, err := lib.DeserializePubKeyToUint64Map(tradingFeesByPkidBytes) + if err != nil { + return nil, fmt.Errorf( + "GetTradingFeesForMarket: Problem deserializing trading fees: %v", err) + } + for pkid, feeBasisPoints := range tradingFeesMapByPkid { + pkidBytes := pkid.ToBytes() + if len(pkidBytes) != btcec.PubKeyBytesLenCompressed { + return nil, fmt.Errorf( + "GetTradingFeesForMarket: Problem decoding public key %s: %v", + pkid, err) + } + pubkey := utxoView.GetPublicKeyForPKID(lib.NewPKID(pkidBytes)) + tradingFeesMapPubkey[*lib.NewPublicKey(pubkey)] = feeBasisPoints + } + } + feeMap := map[string]uint64{} + for publicKey, feeBasisPoints := range tradingFeesMapPubkey { + // Convert the pubkey to a base58 string + pkBase58 := lib.PkToString(publicKey[:], params) + feeMap[pkBase58] = feeBasisPoints + } + + return feeMap, nil +} + +func (fes *APIServer) GetDaoCoinMarketFees(ww http.ResponseWriter, req *http.Request) { + decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) + requestData := GetDaoCoinMarketFeesRequest{} + if err := decoder.Decode(&requestData); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("GetDaoCoinMarketFees: Problem parsing request body: %v", err)) + return + } + + utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView() + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("GetDaoCoinMarketFees: Error fetching mempool view: %v", err)) + return + } + + feeMapByPubkey, err := GetTradingFeesForMarket( + utxoView, + fes.Params, + requestData.ProfilePublicKeyBase58Check) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("GetDaoCoinMarketFees: Problem getting trading fees: %v", err)) + return + } + + // Return all the data associated with the transaction in the response + res := GetDaoCoinMarketFeesResponse{ + FeeBasisPointsByPublicKey: feeMapByPubkey, + } + if err = json.NewEncoder(ww).Encode(res); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("GetDaoCoinMarketFees: Problem encoding response as JSON: %v", err)) + return + } +} + +type CurrencyType string + +const ( + CurrencyTypeUsd CurrencyType = "usd" + CurrencyTypeQuote CurrencyType = "quote" + CurrencyTypeBase CurrencyType = "base" +) + +type DAOCoinLimitOrderWithFeeRequest struct { + // The public key of the user who is creating the order + TransactorPublicKeyBase58Check string `safeForLogging:"true"` + + // For a market, there is always a "base" currency and a "quote" currency. The quote + // currency is the unit of account, eg usd, while the base currency is the coin people + // are trying to buy, eg openfund. A market is always denoted as base/quote. Eg openfund/deso + // or deso/dusdc, for example. If you're still confused, look up base and quote currencies + // as it's a common concept in trading. + QuoteCurrencyPublicKeyBase58Check string `safeForLogging:"true"` + BaseCurrencyPublicKeyBase58Check string `safeForLogging:"true"` + + // "bid" or "ask" + OperationType DAOCoinLimitOrderOperationTypeString `safeForLogging:"true"` + + // A choice of "Fill or Kill", "Immediate or Cancel", or "Good Till Cancelled". + // If it's a market order, then "Good Till Cancelled" is not allowed. + FillType DAOCoinLimitOrderFillTypeString `safeForLogging:"true"` + + // A decimal string (ex: 1.23) that represents the exchange rate between the two coins. + // The price should be the amount should be EITHER the amount of quote currency per one + // unit of base currency OR a USD amount per base currency. Eg + // for the deso/dusdc market, where deso is base and dusdc is quote, the price would simply + // be the deso price in usd. For the openfund/deso market, where openfund is base and deso + // is quote, the price would be the openfund price in deso (eg 0.0002 deso to buy one openfund + // coin) OR the openfund price in USD (which will convert to DESO under the hood to place + // the order). Note that PriceCurrencyType="base" doesn't make sense because the base + // currency is what you're buying/selling in the first place. + // + // If the price is 0.0, then the order is assumed to be a market order. + Price string `safeForLogging:"true"` + PriceCurrencyType CurrencyType `safeForLogging:"true"` + + // Quantity must always be specified either in usd, in quote currency, or in base + // currency. For bids, we expect usd or quote. For asks, we expect usd or base currency + // only. + Quantity string `safeForLogging:"true"` + QuantityCurrencyType CurrencyType `safeForLogging:"true"` + + MinFeeRateNanosPerKB uint64 `safeForLogging:"true"` + TransactionFees []TransactionFee `safeForLogging:"true"` + + OptionalPrecedingTransactions []*lib.MsgDeSoTxn `safeForLogging:"true"` +} + +type DAOCoinLimitOrderWithFeeResponse struct { + // The amount in Deso nanos paid in network fees. We consider this independently + // of trading fees. + FeeNanos uint64 + Transaction *lib.MsgDeSoTxn + TransactionHex string + TxnHashHex string + + // The amount represents either the amount being spent (in the case of a buy) or + // the amount being sold (in the case of a sell). For a buy, the amount is in quote + // currency, while for a sell the amount is in base currency. The messages should + // look as follows in the UI: + // - For a buy: "You will spend: {AmountUsd} (= {Amount} {QuoteCurrency})" + // - For a sell: "You will sell: {Amount} {BaseCurrency}" + // We distinguish the "Limit" amount, which is the maximum the order could possibly + // execute, from the "Executed" amount, which is the amount that it actually will + // execute. For a market order, the "Limit" and "Executed" amounts will be the same. + // For a Limit order, the Executed amount will always be less than or equal to the + // Limit amount. + LimitAmount string `safeForLogging:"true"` + LimitAmountCurrencyType CurrencyType `safeForLogging:"true"` + LimitAmountInUsd string `safeForLogging:"true"` + LimitReceiveAmount string `safeForLogging:"true"` + LimitReceiveAmountCurrencyType CurrencyType `safeForLogging:"true"` + LimitReceiveAmountInUsd string `safeForLogging:"true"` + LimitPriceInQuoteCurrency string `safeForLogging:"true"` + LimitPriceInUsd string `safeForLogging:"true"` + + // For a market order, the amount will generally match the amount requested. However, for + // a limit order, the amount may be less than the amount requested if the order was only + // partially filled. + ExecutionAmount string `safeForLogging:"true"` + ExecutionAmountCurrencyType CurrencyType `safeForLogging:"true"` + ExecutionAmountUsd string `safeForLogging:"true"` + ExecutionReceiveAmount string `safeForLogging:"true"` + ExecutionReceiveAmountCurrencyType CurrencyType `safeForLogging:"true"` + ExecutionReceiveAmountUsd string `safeForLogging:"true"` + ExecutionPriceInQuoteCurrency string `safeForLogging:"true"` + ExecutionPriceInUsd string `safeForLogging:"true"` + ExecutionFeePercentage string `safeForLogging:"true"` + ExecutionFeeAmountInQuoteCurrency string `safeForLogging:"true"` + ExecutionFeeAmountInUsd string `safeForLogging:"true"` + + // The total fee percentage the market charges on taker orders (maker fees are zero + // for now). + MarketTotalTradingFeeBasisPoints string + // Trading fees are paid to users based on metadata in the profile. This map states the trading + // fee split for each user who's been allocated trading fees in the profile. + MarketTradingFeeBasisPointsByUserPublicKey map[string]uint64 +} + +// Used by the client to convert as needed +func GetBuyingSellingPkidFromQuoteBasePkids( + quotePkid string, + basePkid string, + side string, +) ( + _buyingCoinPkid string, + _sellingCoinPkid string, + _err error, +) { + + // Kindof annoying. We don't use base/quote currency in consensus, so we have to + // convert from base/quote to this weird buying/selling thing we did. Oh well. + // The rule of thumb is we're selling the base with an ASK and buying the base + // with a bid. + if side == lib.DAOCoinLimitOrderOperationTypeASK.String() { + return quotePkid, basePkid, nil + } else if side == lib.DAOCoinLimitOrderOperationTypeBID.String() { + return basePkid, quotePkid, nil + } else { + return "", "", fmt.Errorf( + "GetBuyingSellingPkidFromQuoteBasePkids: Invalid side: %v", side) + } +} + +// Used by the client to convert as needed +func GetQuoteBasePkidFromBuyingSellingPkids( + buyingPkid string, + sellingPkid string, + side string, +) ( + _quoteCurrencyPkid string, + _baseCurrencyPkid string, + _err error, +) { + // The rule of thumb is we're selling the base with an ask and buying the + // base with a bid. + if side == lib.DAOCoinLimitOrderOperationTypeBID.String() { + return sellingPkid, buyingPkid, nil + } else if side == lib.DAOCoinLimitOrderOperationTypeASK.String() { + return buyingPkid, sellingPkid, nil + } else { + return "", "", fmt.Errorf( + "GetQuoteBasePkidFromBuyingSellingPkids: Invalid side: %v", side) + } +} + +type GetQuoteCurrencyPriceInUsdRequest struct { + QuoteCurrencyPublicKeyBase58Check string `safeForLogging:"true"` +} + +type GetQuoteCurrencyPriceInUsdResponse struct { + UsdPrice string `safeForLogging:"true"` +} + +func (fes *APIServer) GetQuoteCurrencyPriceInUsdEndpoint(ww http.ResponseWriter, req *http.Request) { + decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) + requestData := GetQuoteCurrencyPriceInUsdRequest{} + if err := decoder.Decode(&requestData); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("GetQuoteCurrencyPriceInUsd: Problem parsing request body: %v", err)) + return + } + + usdPrice, err := fes.GetQuoteCurrencyPriceInUsd(requestData.QuoteCurrencyPublicKeyBase58Check) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("GetQuoteCurrencyPriceInUsd: Problem getting quote currency price in USD: %v", err)) + return + } + + res := GetQuoteCurrencyPriceInUsdResponse{UsdPrice: usdPrice} + if err := json.NewEncoder(ww).Encode(res); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("GetQuoteCurrencyPriceInUsd: Problem encoding response: %v", err)) + return + } +} + +func (fes *APIServer) GetQuoteCurrencyPriceInUsd( + quoteCurrencyPublicKey string) (string, error) { + if IsDesoPkid(quoteCurrencyPublicKey) { + desoUsdCents := fes.GetExchangeDeSoPrice() + return fmt.Sprintf("%0.9f", float64(desoUsdCents)/100), nil + } + utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView() + if err != nil { + return "", fmt.Errorf( + "GetQuoteCurrencyPriceInUsd: Error fetching mempool view: %v", err) + } + + pkBytes, _, err := lib.Base58CheckDecode(quoteCurrencyPublicKey) + if err != nil || len(pkBytes) != btcec.PubKeyBytesLenCompressed { + return "", fmt.Errorf( + "GetQuoteCurrencyPriceInUsd: Problem decoding public key %s: %v", + quoteCurrencyPublicKey, err) + } + + existingProfileEntry := utxoView.GetProfileEntryForPublicKey(pkBytes) + if existingProfileEntry == nil || existingProfileEntry.IsDeleted() { + return "", fmt.Errorf( + "GetQuoteCurrencyPriceInUsd: Profile for quote currency public "+ + "key %v does not exist", + quoteCurrencyPublicKey) + } + + // If the profile is the dusdc profile then just return 1.0 + lowerUsername := strings.ToLower(string(existingProfileEntry.Username)) + if lowerUsername == "dusdc_" { + return "1.0", nil + } else if lowerUsername == "focus" || + lowerUsername == "openfund" { + + desoUsdCents := fes.GetExchangeDeSoPrice() + 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) + 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 + } + } + if highestBidPrice != 0.0 && lowestAskPrice != math.MaxFloat64 { + midPriceDeso := (highestBidPrice + lowestAskPrice) / 2.0 + midPriceUsd := midPriceDeso * float64(desoUsdCents) / 100 + + return fmt.Sprintf("%0.9f", midPriceUsd), nil + } + + return "", fmt.Errorf("GetQuoteCurrencyPriceInUsd: Error calculating price") + } + + return "", fmt.Errorf( + "GetQuoteCurrencyPriceInUsd: Quote currency %v not supported", + quoteCurrencyPublicKey) +} + +func (fes *APIServer) CreateMarketOrLimitOrder( + isMarketOrder bool, + request *DAOCoinLimitOrderCreationRequest, +) ( + *DAOCoinLimitOrderResponse, + error, +) { + + if isMarketOrder { + // We need to translate the req into a DAOCoinMarketOrderCreationRequest + daoCoinMarketOrderRequest := &DAOCoinMarketOrderCreationRequest{ + TransactorPublicKeyBase58Check: request.TransactorPublicKeyBase58Check, + BuyingDAOCoinCreatorPublicKeyBase58Check: request.BuyingDAOCoinCreatorPublicKeyBase58Check, + SellingDAOCoinCreatorPublicKeyBase58Check: request.SellingDAOCoinCreatorPublicKeyBase58Check, + Quantity: request.Quantity, + OperationType: request.OperationType, + FillType: request.FillType, + MinFeeRateNanosPerKB: request.MinFeeRateNanosPerKB, + TransactionFees: request.TransactionFees, + } + + marketOrderRes, err := fes.createDaoCoinMarketOrderHelper(daoCoinMarketOrderRequest) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem creating market order: %v", err) + } + + return marketOrderRes, nil + } else { + limitOrderRes, err := fes.createDaoCoinLimitOrderHelper(request) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem creating market order: %v", err) + } + + return limitOrderRes, nil + } +} + +// priceStr is a decimal string representing the price in quote currency. +func InvertPriceStr(priceStr string) (string, error) { + // - 1.0 / price + // = [1e38 * 1e38 / (price * 1e38)] / 1e38 + scaledPrice, err := lib.CalculateScaledExchangeRateFromString(priceStr) + if err != nil { + return "", fmt.Errorf("HandleMarketOrder: Problem calculating scaled price: %v", err) + } + if scaledPrice.IsZero() { + return "0", err + } + oneE38Squared := big.NewInt(0).Mul(lib.OneE38.ToBig(), lib.OneE38.ToBig()) + invertedScaledPrice := big.NewInt(0).Div(oneE38Squared, scaledPrice.ToBig()) + return lib.FormatScaledUint256AsDecimalString(invertedScaledPrice, lib.OneE38.ToBig()), nil +} + +func (fes *APIServer) SendCoins( + coinPublicKey string, + transactorPubkeyBytes []byte, + receiverPubkeyBytes []byte, + amountBaseUnits *uint256.Int, + minFeeRateNanosPerKb uint64, + additionalOutputs []*lib.DeSoOutput, +) ( + *lib.MsgDeSoTxn, + error, +) { + coinPkBytes, _, err := lib.Base58CheckDecode(coinPublicKey) + if err != nil || len(coinPkBytes) != btcec.PubKeyBytesLenCompressed { + return nil, fmt.Errorf("HandleMarketOrder: Problem decoding coin pkid %s: %v", coinPublicKey, err) + } + + var txn *lib.MsgDeSoTxn + if IsDesoPkid(coinPublicKey) { + txn, _, _, _, _, err = fes.CreateSendDesoTxn( + int64(amountBaseUnits.Uint64()), + transactorPubkeyBytes, + receiverPubkeyBytes, + nil, + minFeeRateNanosPerKb, + additionalOutputs) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem creating transaction: %v", err) + } + } else { + txn, _, _, _, err = fes.blockchain.CreateDAOCoinTransferTxn( + transactorPubkeyBytes, + &lib.DAOCoinTransferMetadata{ + ProfilePublicKey: coinPkBytes, + ReceiverPublicKey: receiverPubkeyBytes, + DAOCoinToTransferNanos: *amountBaseUnits, + }, + // Standard transaction fields + minFeeRateNanosPerKb, + fes.backendServer.GetMempool(), + additionalOutputs) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem creating transaction: %v", err) + } + } + + return txn, nil +} + +func (fes *APIServer) HandleMarketOrder( + isMarketOrder bool, + req *DAOCoinLimitOrderWithFeeRequest, + isBuyOrder bool, + feeMapByPubkey map[string]uint64, +) ( + *DAOCoinLimitOrderWithFeeResponse, + error, +) { + quoteCurrencyUsdValue := float64(0.0) + quoteCurrencyUsdValueStr, err := fes.GetQuoteCurrencyPriceInUsd( + req.QuoteCurrencyPublicKeyBase58Check) + if err != nil { + // If we can't get the price of the quote currency in usd, then we can't + // convert the usd amount to a quote currency amount. In this case, keep + // going but don't use the quote currency usd value for anything. + quoteCurrencyUsdValue = 0.0 + } else { + quoteCurrencyUsdValue, err = strconv.ParseFloat(quoteCurrencyUsdValueStr, 64) + if err != nil { + // Again, get the usd value on a best-effort basis + quoteCurrencyUsdValue = 0.0 + } + } + convertToUsd := func(quoteAmountStr string) string { + quoteAmount, err := strconv.ParseFloat(quoteAmountStr, 64) + if err != nil { + return "" + } + return fmt.Sprintf("%.9f", quoteAmount*quoteCurrencyUsdValue) + } + + quantityStr := req.Quantity + if req.QuantityCurrencyType == CurrencyTypeUsd { + // In this case we want to convert the usd amount to an amount of quote + // currency in base units. To do this we need to get the price of the + // quote currency in usd and then convert the usd amount to quote currency + // amount. + if quoteCurrencyUsdValue == 0.0 { + return nil, fmt.Errorf("HandleMarketOrder: Quote currency price in " + + "usd not available. Please use quote or base currency for the amount.") + } + // For the rest it's just the following formula: + // = usd amount / quoteCurrencyUsdValue * base units + + // In this case we parse the quantity as a simple float since its value + // should not be extreme + quantityUsd, err := strconv.ParseFloat(req.Quantity, 64) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem converting quantity "+ + "to float %v", err) + } + quantityStr = fmt.Sprintf("%0.9f", quantityUsd/quoteCurrencyUsdValue) + } + + priceStrQuote := "" + if !isMarketOrder { + if req.PriceCurrencyType == CurrencyTypeUsd { + // In this case we want to convert the usd amount to an amount of quote + // currency in base units. To do this we need to get the price of the + // quote currency in usd and then convert the usd amount to quote currency + // amount. + if quoteCurrencyUsdValue == 0.0 { + return nil, fmt.Errorf("HandleMarketOrder: Quote currency price in " + + "usd not available. Please use quote or base currency for the amount.") + } + // For the rest it's just the following formula: + // = usd amount / quoteCurrencyUsdValue * base units + + // In this case we parse the quantity as a simple float since its value + // should not be extreme + priceUsd, err := strconv.ParseFloat(req.Price, 64) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem converting price "+ + "to float %v", err) + } + priceStrQuote = fmt.Sprintf("%0.9f", priceUsd/quoteCurrencyUsdValue) + } else if req.PriceCurrencyType == CurrencyTypeQuote { + // This is the easy case. If the price is in quote currency, then we + // can just use it directly. + priceStrQuote = req.Price + } else { + return nil, fmt.Errorf("HandleMarketOrder: Invalid price currency type %v."+ + "Options are 'usd' or 'quote'", + req.PriceCurrencyType) + } + } + + // Next we set the operation type, buying public key, and selling public key based on + // the currency type of the amount. This is confusing, but the reason we need to do it + // this way is because consensus requires that the buying currency be used as the quantity + // for a bid and vice versa for an ask. This causes some bs here. + var operationType DAOCoinLimitOrderOperationTypeString + buyingPublicKey := "" + sellingPublicKey := "" + priceStrConsensus := priceStrQuote + if req.QuantityCurrencyType == CurrencyTypeBase { + if isBuyOrder { + // If you're buying base currency, then the buying coin is the + // base and the operationType is bid. This is the easy case. + operationType = DAOCoinLimitOrderOperationTypeStringBID + buyingPublicKey = req.BaseCurrencyPublicKeyBase58Check + sellingPublicKey = req.QuoteCurrencyPublicKeyBase58Check + } else { + // If you're selling base currency, then the selling coin is the + // base and we can do a vanilla ask. This is another easy case. + operationType = DAOCoinLimitOrderOperationTypeStringASK + buyingPublicKey = req.QuoteCurrencyPublicKeyBase58Check + sellingPublicKey = req.BaseCurrencyPublicKeyBase58Check + } + } else if req.QuantityCurrencyType == CurrencyTypeQuote || + req.QuantityCurrencyType == CurrencyTypeUsd { + if isBuyOrder { + // This is where things get weird. If you're buying the base + // and you want to use quote currency as the quantity, then + // you need to do an ask where the selling currency is the quote. + operationType = DAOCoinLimitOrderOperationTypeStringASK + buyingPublicKey = req.BaseCurrencyPublicKeyBase58Check + sellingPublicKey = req.QuoteCurrencyPublicKeyBase58Check + // We also have to invert the price because consensus assumes the + // denominator is the selling coin for an ask, when it should be + // the base currency. + priceStrConsensus, err = InvertPriceStr(priceStrQuote) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem inverting price: %v", err) + } + } else { + // The last hard case. If you're selling the base and you want + // to use quote currency as the quantity, then you need to do a + // bid where the buying currency is the quote. + operationType = DAOCoinLimitOrderOperationTypeStringBID + buyingPublicKey = req.QuoteCurrencyPublicKeyBase58Check + sellingPublicKey = req.BaseCurrencyPublicKeyBase58Check + // We also have to invert the price because consensus assumes the + // denominator is the buying coin for a bid, when it should be + // the base currency. + priceStrConsensus, err = InvertPriceStr(priceStrQuote) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem inverting price: %v", err) + } + } + } else { + return nil, fmt.Errorf("HandleMarketOrder: Invalid quantity currency type %v", + req.QuantityCurrencyType) + } + + // We need to translate the req into a DAOCoinMarketOrderCreationRequest + daoCoinMarketOrderRequest := &DAOCoinLimitOrderCreationRequest{ + TransactorPublicKeyBase58Check: req.TransactorPublicKeyBase58Check, + BuyingDAOCoinCreatorPublicKeyBase58Check: buyingPublicKey, + SellingDAOCoinCreatorPublicKeyBase58Check: sellingPublicKey, + Quantity: quantityStr, + OperationType: operationType, + Price: priceStrConsensus, + FillType: req.FillType, + MinFeeRateNanosPerKB: req.MinFeeRateNanosPerKB, + TransactionFees: req.TransactionFees, + } + orderRes, err := fes.CreateMarketOrLimitOrder( + isMarketOrder, + daoCoinMarketOrderRequest) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem creating order: %v", err) + } + + quoteCurrencyExecutedBeforeFeesStr := orderRes.SimulatedExecutionResult.BuyingCoinQuantityFilled + if daoCoinMarketOrderRequest.SellingDAOCoinCreatorPublicKeyBase58Check == req.QuoteCurrencyPublicKeyBase58Check { + quoteCurrencyExecutedBeforeFeesStr = orderRes.SimulatedExecutionResult.SellingCoinQuantityFilled + } + + // Now we know how much of the buying and selling currency are going to be transacted. This + // allows us to compute a fee to charge the transactor. + quoteCurrencyExecutedBeforeFeesBaseUnits, err := CalculateBaseUnitsFromStringDecimalAmountSimple( + req.QuoteCurrencyPublicKeyBase58Check, quoteCurrencyExecutedBeforeFeesStr) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating quote currency total: %v", err) + } + + // Compute how much in quote currency we need to pay each constituent + feeBaseUnitsByPubkey := make(map[string]*uint256.Int) + totalFeeBaseUnits := uint256.NewInt() + for pubkey, feeBasisPoints := range feeMapByPubkey { + feeBaseUnits, err := lib.SafeUint256().Mul( + quoteCurrencyExecutedBeforeFeesBaseUnits, uint256.NewInt().SetUint64(feeBasisPoints)) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating fee for quote: %v", err) + } + feeBaseUnits, err = lib.SafeUint256().Div(feeBaseUnits, uint256.NewInt().SetUint64(10000)) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating fee div: %v", err) + } + feeBaseUnitsByPubkey[pubkey] = feeBaseUnits + totalFeeBaseUnits, err = lib.SafeUint256().Add(totalFeeBaseUnits, feeBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating total fee add: %v", err) + } + } + + // Validate that the totalFeeBaseUnits is less than or equal to the quote currency total + if totalFeeBaseUnits.Cmp(quoteCurrencyExecutedBeforeFeesBaseUnits) > 0 { + return nil, fmt.Errorf("HandleMarketOrder: Total fees exceed total quote currency") + } + + // Precompute the total fee to return it later + marketTakerFeeBaseUnits := uint64(0) + for _, feeBaseUnits := range feeMapByPubkey { + marketTakerFeeBaseUnits += feeBaseUnits + } + marketTakerFeeBaseUnitsStr := fmt.Sprintf("%d", marketTakerFeeBaseUnits) + + // Now we have two possibilities... + // + // 1. buy + // The user is buying the base currency with the quote currency. In this case we can + // simply deduct the quote currency from the user's balance prior to executing the + // order, and then execute the order with remainingQuoteCurrencyBaseUnits. + // + // 2. sell + // In this case the user is selling the base currency for quote currency. In this case, + // we need to execute the order first and then deduct the quote currency fee from the + // user's balance after the order has been executed. + // + // Get a universal view to validate as we go + utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView() + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Error fetching mempool view: %v", err) + } + transactorPubkeyBytes, _, err := lib.Base58CheckDecode(req.TransactorPublicKeyBase58Check) + if err != nil || len(transactorPubkeyBytes) != btcec.PubKeyBytesLenCompressed { + return nil, fmt.Errorf("HandleMarketOrder: Problem decoding public key %s: %v", + req.TransactorPublicKeyBase58Check, err) + } + quoteCurrencyPubkeyBytes, _, err := lib.Base58CheckDecode(req.QuoteCurrencyPublicKeyBase58Check) + if err != nil || len(quoteCurrencyPubkeyBytes) != btcec.PubKeyBytesLenCompressed { + return nil, fmt.Errorf("HandleMarketOrder: Problem decoding public key %s: %v", + req.QuoteCurrencyPublicKeyBase58Check, err) + } + if isBuyOrder { + // For each trading fee we need to pay, construct a transfer txn that sends the amount + // from the transactor directly to the person receiving the fee. + transferTxns := []*lib.MsgDeSoTxn{} + for pubkey, feeBaseUnits := range feeBaseUnitsByPubkey { + receiverPubkeyBytes, _, err := lib.Base58CheckDecode(pubkey) + if err != nil || len(receiverPubkeyBytes) != btcec.PubKeyBytesLenCompressed { + return nil, fmt.Errorf("HandleMarketOrder: Problem decoding public key %s: %v", + pubkey, err) + } + // Try and create the TransferDaoCoin transaction for the user. + // + // TODO: Add ExtraData to the transaction to make it easier to report it as an + // earning to the user who's receiving the fee. + txn, err := fes.SendCoins( + req.QuoteCurrencyPublicKeyBase58Check, + transactorPubkeyBytes, + receiverPubkeyBytes, + feeBaseUnits, + req.MinFeeRateNanosPerKB, + nil) + _, _, _, _, err = utxoView.ConnectTransaction( + txn, txn.Hash(), fes.blockchain.BlockTip().Height, + fes.blockchain.BlockTip().Header.TstampNanoSecs, + false, false) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem connecting transaction: %v", err) + } + transferTxns = append(transferTxns, txn) + } + + // Specifying the quantity after deducting fees is a bit tricky. If the user specified the + // original quantity in quote currency, then we can subtract the fee and execute the order + // with what remains after the fee. However, if they specified the original quantity in base + // currency, then we want to convert to quote currency and subtract the fee if we can. However, + // we can only do this if the user specified a price. If they didn't specify a price, then we + // need to fall back on the simulated amount, which is OK since this is a market order anyway. + var remainingQuoteQuantityDecimal string + if req.QuantityCurrencyType == CurrencyTypeQuote || req.QuantityCurrencyType == CurrencyTypeUsd { + // In this case, quantityStr is the amount that the order executed with + // originally. So we deduct the fees from that and run. + quoteCurrencyQuantityTotalBaseUnits, err := CalculateBaseUnitsFromStringDecimalAmountSimple( + req.QuoteCurrencyPublicKeyBase58Check, quantityStr) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating quote currency total: %v", err) + } + quoteCurrencyQuantityMinusFeesBaseUnits, err := lib.SafeUint256().Sub( + quoteCurrencyQuantityTotalBaseUnits, totalFeeBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating remaining quote currency 1: %v", err) + } + remainingQuoteQuantityDecimal, err = CalculateStringDecimalAmountFromBaseUnitsSimple( + req.QuoteCurrencyPublicKeyBase58Check, quoteCurrencyQuantityMinusFeesBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating remaining quote currency 2: %v", err) + } + } else if req.QuantityCurrencyType == CurrencyTypeBase { + // In this case the user specified base currency. If there's a price then try and estimate + // the quote amount. Otherwise, just use the simulated amount. + if priceStrQuote != "" { + // TODO: This is the same as the limit amount calculation below. We should refactor + // this to avoid duplication. + // TODO: This codepath results in the fee percentage being a little lower because we're not + // directly deducting the fees from the amount filled. This is OK for now, and it's + // tough to make it work otherwise. Instead of (feePercent * quantity) -> filledAmount, + // it ends up being: + // - (1+feePercent)(quantity) -> filledAmount, or + // - (quantity) -> filledAmount / (1 + feePercent) + // which is a lower actual fee. I think it's fine for now though. Fixing it would require + // doing two passes to compute the fee, which isn't worth it right now. + // + // In this case the quantityStr needs to be converted from base to quote currency: + // - scaledPrice := priceQuotePerBase * 1e38 + // - quantityBaseUnits * scaledPrice / 1e38 + // + // This multiplies the scaled price by 1e38 then we have to reverse it later + scaledPrice, err := lib.CalculateScaledExchangeRateFromString(priceStrQuote) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating scaled price: %v", err) + } + totalQuantityBaseCurrencyBaseUnits, err := CalculateBaseUnitsFromStringDecimalAmountSimple( + req.BaseCurrencyPublicKeyBase58Check, quantityStr) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating base units: %v", err) + } + bigLimitAmount := big.NewInt(0).Mul(totalQuantityBaseCurrencyBaseUnits.ToBig(), scaledPrice.ToBig()) + bigLimitAmount = big.NewInt(0).Div(bigLimitAmount, lib.OneE38.ToBig()) + uint256LimitAmount := uint256.NewInt() + if overflow := uint256LimitAmount.SetFromBig(bigLimitAmount); overflow { + return nil, fmt.Errorf("HandleMarketOrder: Overflow calculating limit amount") + } + // Subtract the fees from the total quantity + totalQuantityQuoteCurrencyAfterFeesBaseUnits, err := lib.SafeUint256().Sub( + uint256LimitAmount, totalFeeBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating remaining quote currency 1: %v", err) + } + remainingQuoteQuantityDecimal, err = CalculateStringDecimalAmountFromBaseUnitsSimple( + req.QuoteCurrencyPublicKeyBase58Check, totalQuantityQuoteCurrencyAfterFeesBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating remaining quote currency 2: %v", err) + } + } else { + // If there's no price quote then use the simulated amount, minus fees + quotCurrencyExecutedAfterFeesBaseUnits, err := lib.SafeUint256().Sub( + quoteCurrencyExecutedBeforeFeesBaseUnits, totalFeeBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating remaining quote currency 3: %v", err) + } + remainingQuoteQuantityDecimal, err = CalculateStringDecimalAmountFromBaseUnitsSimple( + req.QuoteCurrencyPublicKeyBase58Check, quotCurrencyExecutedAfterFeesBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating remaining quote currency 4: %v", err) + } + } + } else { + // Just to be safe, catch an error here + return nil, fmt.Errorf("HandleMarketOrder: Invalid quantity currency type %v", + req.QuantityCurrencyType) + } + if remainingQuoteQuantityDecimal == "" { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating remaining quote currency 5") + } + + // Now we need to execute the order with the remaining quote currency. + // To make this simple and exact, we can do this as an ask where we are + // selling the quote currency for base currency. This allows us to specify + // the amount of quote currency as the quantity. To make this work we must + // also set the price to the inverse of the quote price because ask orders + // specify their price in (buying coin per selling coin), which in this case + // is (base / quote), or the inversion of priceQuoteStr. Again consensus is + // confusing sorry about that... + priceStrQuoteInverted, err := InvertPriceStr(priceStrQuote) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem inverting price: %v", err) + } + newDaoCoinMarketOrderRequest := &DAOCoinLimitOrderCreationRequest{ + TransactorPublicKeyBase58Check: req.TransactorPublicKeyBase58Check, + BuyingDAOCoinCreatorPublicKeyBase58Check: req.BaseCurrencyPublicKeyBase58Check, + SellingDAOCoinCreatorPublicKeyBase58Check: req.QuoteCurrencyPublicKeyBase58Check, + Quantity: remainingQuoteQuantityDecimal, + OperationType: DAOCoinLimitOrderOperationTypeStringASK, + Price: priceStrQuoteInverted, + FillType: req.FillType, + MinFeeRateNanosPerKB: req.MinFeeRateNanosPerKB, + TransactionFees: req.TransactionFees, + } + newOrderRes, err := fes.CreateMarketOrLimitOrder( + isMarketOrder, newDaoCoinMarketOrderRequest) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem creating market order: %v", err) + } + // Parse the limit order txn from the response + bb, err := hex.DecodeString(newOrderRes.TransactionHex) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem decoding txn hex: %v", err) + } + txn := &lib.MsgDeSoTxn{} + if err := txn.FromBytes(bb); err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem parsing txn: %v", err) + } + _, _, _, _, err = utxoView.ConnectTransaction( + txn, txn.Hash(), fes.blockchain.BlockTip().Height, + fes.blockchain.BlockTip().Header.TstampNanoSecs, + false, false) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem connecting transaction: %v", err) + } + + allTxns := append(transferTxns, newOrderRes.Transaction) + + // Wrap all of the resulting txns into an atomic + // TODO: We can embed helpful extradata in here that will allow us to index these txns + // more coherently + extraData := make(map[string][]byte) + atomicTxn, totalDesoFeeNanos, err := fes.blockchain.CreateAtomicTxnsWrapper( + allTxns, extraData, fes.backendServer.GetMempool(), req.MinFeeRateNanosPerKB) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem creating atomic txn: %v", err) + } + atomixTxnBytes, err := atomicTxn.ToBytes(true) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem serializing atomic txn: %v", err) + } + atomicTxnHex := hex.EncodeToString(atomixTxnBytes) + + // Now that we've executed the order, we have everything we need to return to the UI + // so it can display the order to the user. + + // This is tricky. The execution amount is the amount that was simulated from the order PLUS + // the amount we deducted in fees prior to executing the order. + // + // We know the quote currency executed amount is the selling coin quantity filled because it's + // how we set up the order request. + quoteCurrencyExecutedAfterFeesStr := newOrderRes.SimulatedExecutionResult.SellingCoinQuantityFilled + quoteCurrencyExecutedAfterFeesBaseUnits, err := CalculateBaseUnitsFromStringDecimalAmountSimple( + req.QuoteCurrencyPublicKeyBase58Check, quoteCurrencyExecutedAfterFeesStr) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating quote currency total 1: %v", err) + } + quoteCurrencyExecutedPlusFeesBaseUnits, err := lib.SafeUint256().Add( + quoteCurrencyExecutedAfterFeesBaseUnits, totalFeeBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating quote currency total 2: %v", err) + } + executionAmount, err := CalculateStringDecimalAmountFromBaseUnitsSimple( + req.QuoteCurrencyPublicKeyBase58Check, quoteCurrencyExecutedPlusFeesBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating quote currency spent: %v", err) + } + executionAmountCurrencyType := CurrencyTypeQuote + // The receive amount is the buying coin quantity filled because that's how we set up the order. + executionReceiveAmount := newOrderRes.SimulatedExecutionResult.BuyingCoinQuantityFilled + executionReceiveAmountCurrencyType := CurrencyTypeBase + executionReceiveAmountBaseUnits, err := CalculateBaseUnitsFromStringDecimalAmountSimple( + req.BaseCurrencyPublicKeyBase58Check, executionReceiveAmount) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating base currency received: %v", err) + } + + // The price per token the user is getting, expressed as a decimal float + // - quoteAmountSpentTotal / baseAmountReceived + // - = (quoteAmountSpentTotal * BaseUnitsPerCoin / baseAmountReceived) / BaseUnitsPerCoin + executionPriceInQuoteCurrency := "" + if !executionReceiveAmountBaseUnits.IsZero() { + priceQuotePerBase := big.NewInt(0).Mul( + quoteCurrencyExecutedPlusFeesBaseUnits.ToBig(), lib.BaseUnitsPerCoin.ToBig()) + priceQuotePerBase = big.NewInt(0).Div( + priceQuotePerBase, executionReceiveAmountBaseUnits.ToBig()) + executionPriceInQuoteCurrency = lib.FormatScaledUint256AsDecimalString( + priceQuotePerBase, lib.BaseUnitsPerCoin.ToBig()) + } + + // Compute the percentage of the amount spent that went to fees + // - totalFeeBaseUnits / quoteAmountSpentTotalBaseUnits + // - = (totalFeeBaseUnits * BaseUnitsPerCoin / quoteAmountSpentTotalBaseUnits) / BaseUnitsPerCoin + executionFeePercentage := "" + if !quoteCurrencyExecutedPlusFeesBaseUnits.IsZero() { + percentageSpentOnFees := big.NewInt(0).Mul( + totalFeeBaseUnits.ToBig(), lib.BaseUnitsPerCoin.ToBig()) + percentageSpentOnFees = big.NewInt(0).Div( + percentageSpentOnFees, quoteCurrencyExecutedPlusFeesBaseUnits.ToBig()) + executionFeePercentage = lib.FormatScaledUint256AsDecimalString( + percentageSpentOnFees, lib.BaseUnitsPerCoin.ToBig()) + } + + executionFeeAmountInQuoteCurrency, err := CalculateStringDecimalAmountFromBaseUnitsSimple( + req.QuoteCurrencyPublicKeyBase58Check, totalFeeBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating fee: %v", err) + } + + res := &DAOCoinLimitOrderWithFeeResponse{ + // The amount in Deso nanos paid in network fees. We consider this independently + // of trading fees. + FeeNanos: totalDesoFeeNanos, + Transaction: atomicTxn, + TransactionHex: atomicTxnHex, + TxnHashHex: txn.Hash().String(), + + // For a market order, the amount will generally match the amount requested. However, for + // a limit order, the amount may be less than the amount requested if the order was only + // partially filled. + ExecutionAmount: executionAmount, + ExecutionAmountCurrencyType: executionAmountCurrencyType, + ExecutionAmountUsd: convertToUsd(executionAmount), + ExecutionReceiveAmount: executionReceiveAmount, + ExecutionReceiveAmountCurrencyType: executionReceiveAmountCurrencyType, + ExecutionReceiveAmountUsd: "", // dont convert base currency to usd + ExecutionPriceInQuoteCurrency: executionPriceInQuoteCurrency, + ExecutionPriceInUsd: convertToUsd(executionPriceInQuoteCurrency), + ExecutionFeePercentage: executionFeePercentage, + ExecutionFeeAmountInQuoteCurrency: executionFeeAmountInQuoteCurrency, + ExecutionFeeAmountInUsd: convertToUsd(executionFeeAmountInQuoteCurrency), + + MarketTotalTradingFeeBasisPoints: marketTakerFeeBaseUnitsStr, + MarketTradingFeeBasisPointsByUserPublicKey: feeMapByPubkey, + } + + if !isMarketOrder { + // The quantityStr is in quote currency or base units. If it's in base units then + // we need to do a conversion into quote currency. + limitAmount := quantityStr + if req.QuantityCurrencyType == CurrencyTypeBase { + // In this case the quantityStr needs to be converted from base to quote currency: + // - scaledPrice := priceQuotePerBase * 1e38 + // - quantityBaseUnits * scaledPrice / 1e38 + // + // This multiplies the scaled price by 1e38 then we have to reverse it later + scaledPrice, err := lib.CalculateScaledExchangeRateFromString(priceStrQuote) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating scaled price: %v", err) + } + quantityBaseUnits, err := CalculateBaseUnitsFromStringDecimalAmountSimple( + req.BaseCurrencyPublicKeyBase58Check, quantityStr) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating base units: %v", err) + } + bigLimitAmount := big.NewInt(0).Mul(quantityBaseUnits.ToBig(), scaledPrice.ToBig()) + bigLimitAmount = big.NewInt(0).Div(bigLimitAmount, lib.OneE38.ToBig()) + uint256LimitAmount := uint256.NewInt() + if overflow := uint256LimitAmount.SetFromBig(bigLimitAmount); overflow { + return nil, fmt.Errorf("HandleMarketOrder: Overflow calculating limit amount") + } + limitAmount, err = CalculateStringDecimalAmountFromBaseUnitsSimple( + req.QuoteCurrencyPublicKeyBase58Check, uint256LimitAmount) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating limit amount: %v", err) + } + } + + // The limit receive amount is computed as follows: + // - limitAmount / price + // - = limitAmount * 1e38 / (price * 1e38) + limitReceiveAmountBaseUnits, err := CalculateBaseUnitsFromStringDecimalAmountSimple( + req.QuoteCurrencyPublicKeyBase58Check, limitAmount) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating limit receive amount: %v", err) + } + bigLimitReceiveAmount := big.NewInt(0).Mul(limitReceiveAmountBaseUnits.ToBig(), lib.OneE38.ToBig()) + // This multiplies the scaled price by 1e38 then we have to reverse it later + scaledPrice, err := lib.CalculateScaledExchangeRateFromString(priceStrQuote) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating scaled price: %v", err) + } + limitReceiveAmount := "" + if !scaledPrice.IsZero() { + bigLimitReceiveAmount = big.NewInt(0).Div(bigLimitReceiveAmount, scaledPrice.ToBig()) + uint256LimitReceiveAmount := uint256.NewInt() + if overflow := uint256LimitReceiveAmount.SetFromBig(bigLimitReceiveAmount); overflow { + return nil, fmt.Errorf("HandleMarketOrder: Overflow calculating limit receive amount") + } + limitReceiveAmount, err = CalculateStringDecimalAmountFromBaseUnitsSimple( + req.BaseCurrencyPublicKeyBase58Check, uint256LimitReceiveAmount) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating limit receive amount: %v", err) + } + } + + // Set all the values we calculated + res.LimitAmount = limitAmount + res.LimitAmountCurrencyType = CurrencyTypeQuote + res.LimitAmountInUsd = convertToUsd(limitAmount) + res.LimitReceiveAmount = limitReceiveAmount + res.LimitReceiveAmountCurrencyType = CurrencyTypeBase + res.LimitReceiveAmountInUsd = "" // dont convert base currency to usd + res.LimitPriceInQuoteCurrency = priceStrQuote + res.LimitPriceInUsd = convertToUsd(priceStrQuote) + } + + return res, nil + } else { + // We already have the txn that executes the order from previously + // Connect it to our UtxoView for validation + bb, err := hex.DecodeString(orderRes.TransactionHex) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem decoding txn hex: %v", err) + } + orderTxn := &lib.MsgDeSoTxn{} + if err := orderTxn.FromBytes(bb); err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem parsing txn: %v", err) + } + _, _, _, _, err = utxoView.ConnectTransaction( + orderTxn, orderTxn.Hash(), fes.blockchain.BlockTip().Height, + fes.blockchain.BlockTip().Header.TstampNanoSecs, + false, false) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem connecting transaction: %v", err) + } + + // Now we need to deduct the fees from the user's balance. + // For each trading fee we need to pay, construct a transfer txn that sends the amount + // from the transactor directly to the person receiving the fee. + transferTxns := []*lib.MsgDeSoTxn{} + for pubkey, feeBaseUnits := range feeBaseUnitsByPubkey { + receiverPubkeyBytes, _, err := lib.Base58CheckDecode(pubkey) + if err != nil || len(receiverPubkeyBytes) != btcec.PubKeyBytesLenCompressed { + return nil, fmt.Errorf("HandleMarketOrder: Problem decoding public key %s: %v", + pubkey, err) + } + // Try and create the TransferDaoCoin transaction for the user. + // + // TODO: Add ExtraData to the transaction to make it easier to report it as an + // earning to the user who's receiving the fee. + txn, _, _, _, err := fes.blockchain.CreateDAOCoinTransferTxn( + transactorPubkeyBytes, + &lib.DAOCoinTransferMetadata{ + ProfilePublicKey: quoteCurrencyPubkeyBytes, + ReceiverPublicKey: receiverPubkeyBytes, + DAOCoinToTransferNanos: *feeBaseUnits, + }, + // Standard transaction fields + req.MinFeeRateNanosPerKB, fes.backendServer.GetMempool(), nil) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem creating transaction: %v", err) + } + _, _, _, _, err = utxoView.ConnectTransaction( + txn, txn.Hash(), fes.blockchain.BlockTip().Height, + fes.blockchain.BlockTip().Header.TstampNanoSecs, + false, false) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem connecting transaction: %v", err) + } + transferTxns = append(transferTxns, txn) + } + + // Wrap all of the resulting txns into an atomic + allTxns := append(transferTxns, orderTxn) + extraData := make(map[string][]byte) + atomicTxn, totalDesoFeeNanos, err := fes.blockchain.CreateAtomicTxnsWrapper( + allTxns, extraData, fes.backendServer.GetMempool(), req.MinFeeRateNanosPerKB) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem creating atomic txn: %v", err) + } + atomixTxnBytes, err := atomicTxn.ToBytes(true) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem serializing atomic txn: %v", err) + } + atomicTxnHex := hex.EncodeToString(atomixTxnBytes) + + // Now that we've executed the order, we have everything we need to return to the UI + totalFeeStr, err := CalculateStringDecimalAmountFromBaseUnitsSimple( + req.QuoteCurrencyPublicKeyBase58Check, totalFeeBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating fee: %v", err) + } + quoteAmountReceivedBaseUnits, err := lib.SafeUint256().Sub( + quoteCurrencyExecutedBeforeFeesBaseUnits, totalFeeBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating quote currency received: %v", err) + } + quoteAmountReceivedStr, err := CalculateStringDecimalAmountFromBaseUnitsSimple( + req.QuoteCurrencyPublicKeyBase58Check, quoteAmountReceivedBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating quote currency received: %v", err) + } + baseAmountSpentStr := orderRes.SimulatedExecutionResult.SellingCoinQuantityFilled + if daoCoinMarketOrderRequest.SellingDAOCoinCreatorPublicKeyBase58Check == req.QuoteCurrencyPublicKeyBase58Check { + baseAmountSpentStr = orderRes.SimulatedExecutionResult.BuyingCoinQuantityFilled + } + baseAmountSpentBaseUnits, err := CalculateBaseUnitsFromStringDecimalAmountSimple( + req.BaseCurrencyPublicKeyBase58Check, baseAmountSpentStr) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating base currency spent: %v", err) + } + // The price per token the user is getting, expressed as a decimal float + // - quoteAmountReceived / baseAmountSpent + // - = (quoteAmountReceived * BaseUnitsPerCoin / baseAmountReceived) / BaseUnitsPerCoin + finalPriceStr := "0.0" + if !baseAmountSpentBaseUnits.IsZero() { + priceQuotePerBase := big.NewInt(0).Mul( + quoteAmountReceivedBaseUnits.ToBig(), lib.BaseUnitsPerCoin.ToBig()) + priceQuotePerBase = big.NewInt(0).Div( + priceQuotePerBase, baseAmountSpentBaseUnits.ToBig()) + finalPriceStr = lib.FormatScaledUint256AsDecimalString(priceQuotePerBase, lib.BaseUnitsPerCoin.ToBig()) + } + + // Compute the percentage of the amount spent that went to fees + // - totalFeeBaseUnits / quoteAmountTotalBaseUnits + // - = (totalFeeBaseUnits * BaseUnitsPerCoin / quoteAmountTotalBaseUnits) / BaseUnitsPerCoin + percentageSpentOnFeesStr := "0.0" + if !quoteCurrencyExecutedBeforeFeesBaseUnits.IsZero() { + percentageSpentOnFees := big.NewInt(0).Mul( + totalFeeBaseUnits.ToBig(), lib.BaseUnitsPerCoin.ToBig()) + percentageSpentOnFees = big.NewInt(0).Div( + percentageSpentOnFees, quoteCurrencyExecutedBeforeFeesBaseUnits.ToBig()) + percentageSpentOnFeesStr = lib.FormatScaledUint256AsDecimalString( + percentageSpentOnFees, lib.BaseUnitsPerCoin.ToBig()) + } + + tradingFeesInQuoteCurrencyByPubkey := make(map[string]string) + for pubkey, feeBaseUnits := range feeBaseUnitsByPubkey { + feeStr, err := CalculateStringDecimalAmountFromBaseUnitsSimple( + req.QuoteCurrencyPublicKeyBase58Check, feeBaseUnits) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating fee: %v", err) + } + tradingFeesInQuoteCurrencyByPubkey[pubkey] = feeStr + } + + res := &DAOCoinLimitOrderWithFeeResponse{ + FeeNanos: totalDesoFeeNanos, + TransactionHex: atomicTxnHex, + TxnHashHex: atomicTxn.Hash().String(), + Transaction: atomicTxn, + + ExecutionAmount: baseAmountSpentStr, + ExecutionAmountCurrencyType: CurrencyTypeBase, + ExecutionAmountUsd: "", // dont convert base currency to usd + ExecutionReceiveAmount: quoteAmountReceivedStr, + ExecutionReceiveAmountCurrencyType: CurrencyTypeQuote, + ExecutionReceiveAmountUsd: convertToUsd(quoteAmountReceivedStr), + ExecutionPriceInQuoteCurrency: finalPriceStr, + ExecutionPriceInUsd: convertToUsd(finalPriceStr), + ExecutionFeePercentage: percentageSpentOnFeesStr, + ExecutionFeeAmountInQuoteCurrency: totalFeeStr, + ExecutionFeeAmountInUsd: convertToUsd(totalFeeStr), + + MarketTotalTradingFeeBasisPoints: marketTakerFeeBaseUnitsStr, + // Trading fees are paid to users based on metadata in the profile. This map states the trading + // fee split for each user who's been allocated trading fees in the profile. + MarketTradingFeeBasisPointsByUserPublicKey: feeMapByPubkey, + } + + if !isMarketOrder { + // The quantityStr is in quote currency or base units. If it's in quote currency + // then we need to do a conversion to base units. + limitAmount := quantityStr + if req.QuantityCurrencyType == CurrencyTypeQuote || + req.QuantityCurrencyType == CurrencyTypeUsd { + // In this case we need to convert the quantity to base units: + // - quoteAmount / price + // - = quoteAmount * 1e38 / (price * 1e38) + quantityBaseUnits, err := CalculateBaseUnitsFromStringDecimalAmountSimple( + req.QuoteCurrencyPublicKeyBase58Check, quantityStr) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating base units: %v", err) + } + // This multiplies the scaled price by 1e38 then we have to reverse it later + scaledPrice, err := lib.CalculateScaledExchangeRateFromString(priceStrQuote) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating scaled price: %v", err) + } + limitAmount = "" + if !scaledPrice.IsZero() { + bigLimitAmount := big.NewInt(0).Mul(quantityBaseUnits.ToBig(), lib.OneE38.ToBig()) + bigLimitAmount = big.NewInt(0).Div(bigLimitAmount, scaledPrice.ToBig()) + uint256LimitAmount := uint256.NewInt() + if overflow := uint256LimitAmount.SetFromBig(bigLimitAmount); overflow { + return nil, fmt.Errorf("HandleMarketOrder: Overflow calculating limit amount") + } + limitAmount, err = CalculateStringDecimalAmountFromBaseUnitsSimple( + req.BaseCurrencyPublicKeyBase58Check, uint256LimitAmount) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating limit amount: %v", err) + } + } + } + + // The limit receive amount is computed as follows: + // - limitAmount * price + // - = limitAmount * (price * 1e38) / 1e38 + limitReceiveAmountBaseUnits, err := CalculateBaseUnitsFromStringDecimalAmountSimple( + req.BaseCurrencyPublicKeyBase58Check, limitAmount) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating limit receive amount: %v", err) + } + // This multiplies the scaled price by 1e38 then we have to reverse it later + scaledPrice, err := lib.CalculateScaledExchangeRateFromString(priceStrQuote) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating scaled price: %v", err) + } + bigLimitReceiveAmount := big.NewInt(0).Mul(limitReceiveAmountBaseUnits.ToBig(), scaledPrice.ToBig()) + bigLimitReceiveAmount = big.NewInt(0).Div(bigLimitReceiveAmount, lib.OneE38.ToBig()) + uint256LimitReceiveAmount := uint256.NewInt() + if overflow := uint256LimitReceiveAmount.SetFromBig(bigLimitReceiveAmount); overflow { + return nil, fmt.Errorf("HandleMarketOrder: Overflow calculating limit receive amount") + } + limitReceiveAmount, err := CalculateStringDecimalAmountFromBaseUnitsSimple( + req.QuoteCurrencyPublicKeyBase58Check, uint256LimitReceiveAmount) + if err != nil { + return nil, fmt.Errorf("HandleMarketOrder: Problem calculating limit receive amount: %v", err) + } + + // Set all the values we calculated + res.LimitAmount = limitAmount + res.LimitAmountCurrencyType = CurrencyTypeBase + res.LimitAmountInUsd = "" // dont convert base to usd + res.LimitReceiveAmount = limitReceiveAmount + res.LimitReceiveAmountCurrencyType = CurrencyTypeQuote + res.LimitReceiveAmountInUsd = convertToUsd(res.LimitReceiveAmount) + res.LimitPriceInQuoteCurrency = priceStrQuote + res.LimitPriceInUsd = convertToUsd(priceStrQuote) + } + + return res, nil + } +} + +func (fes *APIServer) CreateDAOCoinLimitOrderWithFee(ww http.ResponseWriter, req *http.Request) { + decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) + requestData := DAOCoinLimitOrderWithFeeRequest{} + + if err := decoder.Decode(&requestData); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrderWithFee: Problem parsing request body: %v", err)) + return + } + + // Swap the deso key for lib.ZeroPkid + if IsDesoPkid(requestData.BaseCurrencyPublicKeyBase58Check) { + requestData.BaseCurrencyPublicKeyBase58Check = lib.PkToString(lib.ZeroPKID[:], fes.Params) + } + if IsDesoPkid(requestData.QuoteCurrencyPublicKeyBase58Check) { + requestData.QuoteCurrencyPublicKeyBase58Check = lib.PkToString(lib.ZeroPKID[:], fes.Params) + } + + // First determine if this is a limit or a market order + isMarketOrder := false + floatPrice, _ := strconv.ParseFloat(requestData.Price, 64) + if floatPrice == 0 { + isMarketOrder = true + } + + // Validate the OperationType + if string(requestData.OperationType) != lib.DAOCoinLimitOrderOperationTypeASK.String() && + string(requestData.OperationType) != lib.DAOCoinLimitOrderOperationTypeBID.String() { + _AddBadRequestError(ww, fmt.Sprintf( + "CreateDAOCoinLimitOrderWithFee: Invalid operation type: %v. Options are: %v, %v", + requestData.OperationType, lib.DAOCoinLimitOrderOperationTypeASK.String(), + lib.DAOCoinLimitOrderOperationTypeBID.String())) + return + } + + // Determine if it's a buy or sell order + isBuyOrder := false + if string(requestData.OperationType) == lib.DAOCoinLimitOrderOperationTypeBID.String() { + isBuyOrder = true + } + + // Validate the fill type + if requestData.FillType != DAOCoinLimitOrderFillTypeFillOrKill && + requestData.FillType != DAOCoinLimitOrderFillTypeImmediateOrCancel && + requestData.FillType != DAOCoinLimitOrderFillTypeGoodTillCancelled { + _AddBadRequestError(ww, fmt.Sprintf( + "CreateDAOCoinLimitOrderWithFee: Invalid fill type: %v. Options are: "+ + "%v, %v, %v", requestData.FillType, DAOCoinLimitOrderFillTypeFillOrKill, + DAOCoinLimitOrderFillTypeImmediateOrCancel, DAOCoinLimitOrderFillTypeGoodTillCancelled)) + return + } + + // If we're dealing with a market order then we don't allow "Good Till Cancelled" + if isMarketOrder && requestData.FillType == DAOCoinLimitOrderFillTypeGoodTillCancelled { + _AddBadRequestError(ww, fmt.Sprintf( + "CreateDAOCoinLimitOrderWithFee: Market orders cannot be Good Till Cancelled")) + return + } + + // Get a universal view to do more sophisticated validation + utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView() + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrderWithFee: Error fetching mempool view: %v", err)) + return + } + + // Get the trading fees for the market. This is the trading fee split for each user + // Only the base currency can have fees on it. The quote currency cannot. + feeMapByPubkey, err := GetTradingFeesForMarket( + utxoView, + fes.Params, + requestData.BaseCurrencyPublicKeyBase58Check) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("GetDaoCoinMarketFees: Problem getting trading fees: %v", err)) + return + } + // Validate the fee map. + if err := ValidateTradingFeeMap(feeMapByPubkey); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateDaoCoinMarketFees: %v", err)) + return + } + + // If the trading user is in the fee map, remove them so that we don't end up + // doing a self-send + if _, exists := feeMapByPubkey[requestData.TransactorPublicKeyBase58Check]; exists { + delete(feeMapByPubkey, requestData.TransactorPublicKeyBase58Check) + } + + var res *DAOCoinLimitOrderWithFeeResponse + res, err = fes.HandleMarketOrder(isMarketOrder, &requestData, isBuyOrder, feeMapByPubkey) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrderWithFee: %v", err)) + return + } + + if err = json.NewEncoder(ww).Encode(res); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrderWithFee: Problem encoding response as JSON: %v", err)) + return + } +} diff --git a/routes/extra_data_utils.go b/routes/extra_data_utils.go index f4679fd7..776f346a 100644 --- a/routes/extra_data_utils.go +++ b/routes/extra_data_utils.go @@ -50,8 +50,9 @@ var specialExtraDataKeysToEncoding = map[string]ExtraDataEncoding{ lib.BuyNowPriceKey: {Decode: Decode64BitUintString, Encode: Encode64BitUintString}, - lib.DESORoyaltiesMapKey: {Decode: DecodePubKeyToUint64MapString, Encode: ReservedFieldCannotEncode}, - lib.CoinRoyaltiesMapKey: {Decode: DecodePubKeyToUint64MapString, Encode: ReservedFieldCannotEncode}, + lib.DESORoyaltiesMapKey: {Decode: DecodePubKeyToUint64MapString, Encode: ReservedFieldCannotEncode}, + lib.CoinRoyaltiesMapKey: {Decode: DecodePubKeyToUint64MapString, Encode: ReservedFieldCannotEncode}, + lib.TokenTradingFeesByPkidMapKey: {Decode: DecodePubKeyToUint64MapString, Encode: ReservedFieldCannotEncode}, lib.MessagesVersionString: {Decode: Decode64BitUintString, Encode: Encode64BitUintString}, diff --git a/routes/server.go b/routes/server.go index 5b4b2a96..098f85b3 100644 --- a/routes/server.go +++ b/routes/server.go @@ -104,6 +104,12 @@ const ( RoutePathGetDaoCoinLimitOrdersById = "/api/v0/get-dao-coin-limit-orders-by-id" RoutePathGetTransactorDaoCoinLimitOrders = "/api/v0/get-transactor-dao-coin-limit-orders" + // dao_coin_exchange_with_fees.go + RoutePathUpdateDaoCoinMarketFees = "/api/v0/update-dao-coin-market-fees" + RoutePathGetDaoCoinMarketFees = "/api/v0/get-dao-coin-market-fees" + RoutePathCreateDAOCoinLimitOrderWithFee = "/api/v0/create-dao-coin-limit-order-with-fee" + RoutePathGetQuoteCurrencyPriceInUsd = "/api/v0/get-quote-currency-price-in-usd" + // post.go RoutePathGetPostsHashHexList = "/api/v0/get-posts-hashhexlist" RoutePathGetPostsStateless = "/api/v0/get-posts-stateless" @@ -1261,6 +1267,34 @@ func (fes *APIServer) NewRouter() *muxtrace.Router { fes.GetTransactorDAOCoinLimitOrders, PublicAccess, }, + { + "UpdateDaoCoinMarketFees", + []string{"POST", "OPTIONS"}, + RoutePathUpdateDaoCoinMarketFees, + fes.UpdateDaoCoinMarketFees, + PublicAccess, + }, + { + "GetDaoCoinMarketFees", + []string{"POST", "OPTIONS"}, + RoutePathGetDaoCoinMarketFees, + fes.GetDaoCoinMarketFees, + PublicAccess, + }, + { + "CreateDAOCoinLimitOrderWithFee", + []string{"POST", "OPTIONS"}, + RoutePathCreateDAOCoinLimitOrderWithFee, + fes.CreateDAOCoinLimitOrderWithFee, + PublicAccess, + }, + { + "GetQuoteCurrencyPriceInUsd", + []string{"POST", "OPTIONS"}, + RoutePathGetQuoteCurrencyPriceInUsd, + fes.GetQuoteCurrencyPriceInUsdEndpoint, + PublicAccess, + }, { "CreateUserAssociation", []string{"POST", "OPTIONS"}, diff --git a/routes/transaction.go b/routes/transaction.go index 7615a027..3d481bdf 100644 --- a/routes/transaction.go +++ b/routes/transaction.go @@ -69,21 +69,42 @@ func (fes *APIServer) GetTxn(ww http.ResponseWriter, req *http.Request) { copy(txnHash[:], txnHashBytes) } - txnFound := false + // The order of operations is tricky here. We need to do the following in this + // exact order: + // 1. Check the mempool for the txn + // 2. Wait for txindex to fully sync + // 3. Then check txindex + // + // If we instead check the mempool afterward, then there is a chance that the txn + // has been removed by a new block that is not yet in txindex. This would cause the + // endpoint to incorrectly report that the txn doesn't exist on the node, when in + // fact it is in "limbo" between the mempool and txindex. txnStatus := requestData.TxnStatus if txnStatus == "" { txnStatus = TxnStatusInMempool } + txnInMempool := fes.backendServer.GetMempool().IsTransactionInPool(txnHash) + startTime := time.Now() + // We have to wait until txindex has reached the uncommitted tip height, not the + // committed tip height. Otherwise we'll be missing ~2 blocks in limbo. + coreChainTipHeight := fes.TXIndex.CoreChain.BlockTip().Height + for fes.TXIndex.TXIndexChain.BlockTip().Height < coreChainTipHeight { + if time.Since(startTime) > 30*time.Second { + _AddBadRequestError(ww, fmt.Sprintf("GetTxn: Timed out waiting for txindex to sync.")) + return + } + time.Sleep(10 * time.Millisecond) + } + txnInTxindex := lib.DbCheckTxnExistence(fes.TXIndex.TXIndexChain.DB(), nil, txnHash) + txnFound := false switch txnStatus { case TxnStatusInMempool: - txnFound = fes.backendServer.GetMempool().IsTransactionInPool(txnHash) - if !txnFound { - txnFound = lib.DbCheckTxnExistence(fes.TXIndex.TXIndexChain.DB(), nil, txnHash) - } + // In this case, we're fine if the txn is either in the mempool or in txindex. + txnFound = txnInMempool || txnInTxindex case TxnStatusCommitted: // In this case we will not consider a txn until it shows up in txindex, which means that // it is committed. - txnFound = lib.DbCheckTxnExistence(fes.TXIndex.TXIndexChain.DB(), nil, txnHash) + txnFound = txnInTxindex default: _AddBadRequestError(ww, fmt.Sprintf("GetTxn: Invalid TxnStatus: %v. Options are "+ "{InMempool, Committed}", txnStatus)) @@ -1222,6 +1243,78 @@ type SendDeSoResponse struct { TxnHashHex string } +func (fes *APIServer) CreateSendDesoTxn( + amountNanos int64, + senderPkBytes []byte, + recipientPkBytes []byte, + extraData map[string][]byte, + minFeeRateNanosPerKb uint64, + additionalOutputs []*lib.DeSoOutput, +) ( + _txn *lib.MsgDeSoTxn, + _totalInput uint64, + _spendAmount uint64, + _changeAmount uint64, + _feeNanos uint64, + _err error, +) { + // If the AmountNanos is less than zero then we have a special case where we create + // a transaction with the maximum spend. + var txnn *lib.MsgDeSoTxn + var totalInputt uint64 + var spendAmountt uint64 + var changeAmountt uint64 + var feeNanoss uint64 + var err error + if amountNanos < 0 { + // Create a MAX transaction + txnn, totalInputt, spendAmountt, feeNanoss, err = fes.blockchain.CreateMaxSpend( + senderPkBytes, recipientPkBytes, extraData, minFeeRateNanosPerKb, + fes.backendServer.GetMempool(), additionalOutputs) + if err != nil { + return nil, 0, 0, 0, 0, fmt.Errorf("CreateSendDesoTxn: Error creating max spend: %v", err) + } + + } else { + // In this case, we are spending what the user asked us to spend as opposed to + // spending the maximum amount possible. + + // Create the transaction outputs and add the recipient's public key and the + // amount we want to pay them + txnOutputs := append(additionalOutputs, &lib.DeSoOutput{ + PublicKey: recipientPkBytes, + // If we get here we know the amount is non-negative. + AmountNanos: uint64(amountNanos), + }) + + // Assemble the transaction so that inputs can be found and fees can + // be computed. + txnn = &lib.MsgDeSoTxn{ + // The inputs will be set below. + TxInputs: []*lib.DeSoInput{}, + TxOutputs: txnOutputs, + PublicKey: senderPkBytes, + TxnMeta: &lib.BasicTransferMetadata{}, + // We wait to compute the signature until we've added all the + // inputs and change. + } + + if len(extraData) > 0 { + txnn.ExtraData = extraData + } + + // Add inputs to the transaction and do signing, validation, and broadcast + // depending on what the user requested. + totalInputt, spendAmountt, changeAmountt, feeNanoss, err = + fes.blockchain.AddInputsAndChangeToTransaction( + txnn, minFeeRateNanosPerKb, fes.backendServer.GetMempool()) + if err != nil { + return nil, 0, 0, 0, 0, fmt.Errorf("CreateSendDesoTxn: Error adding inputs and change to transaction: %v", err) + } + } + return txnn, totalInputt, spendAmountt, changeAmountt, feeNanoss, nil +} + // SendDeSo ... func (fes *APIServer) SendDeSo(ww http.ResponseWriter, req *http.Request) { decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) @@ -1291,61 +1384,13 @@ func (fes *APIServer) SendDeSo(ww http.ResponseWriter, req *http.Request) { return } - // If the AmountNanos is less than zero then we have a special case where we create - // a transaction with the maximum spend. - var txnn *lib.MsgDeSoTxn - var totalInputt uint64 - var spendAmountt uint64 - var changeAmountt uint64 - var feeNanoss uint64 - if requestData.AmountNanos < 0 { - // Create a MAX transaction - txnn, totalInputt, spendAmountt, feeNanoss, err = fes.blockchain.CreateMaxSpend( - senderPkBytes, recipientPkBytes, extraData, requestData.MinFeeRateNanosPerKB, - fes.backendServer.GetMempool(), additionalOutputs) - if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("SendDeSo: Error processing MAX transaction: %v", err)) - return - } - - } else { - // In this case, we are spending what the user asked us to spend as opposed to - // spending the maximum amount possible. - - // Create the transaction outputs and add the recipient's public key and the - // amount we want to pay them - txnOutputs := append(additionalOutputs, &lib.DeSoOutput{ - PublicKey: recipientPkBytes, - // If we get here we know the amount is non-negative. - AmountNanos: uint64(requestData.AmountNanos), - }) - - // Assemble the transaction so that inputs can be found and fees can - // be computed. - txnn = &lib.MsgDeSoTxn{ - // The inputs will be set below. - TxInputs: []*lib.DeSoInput{}, - TxOutputs: txnOutputs, - PublicKey: senderPkBytes, - TxnMeta: &lib.BasicTransferMetadata{}, - // We wait to compute the signature until we've added all the - // inputs and change. - } - - if len(extraData) > 0 { - txnn.ExtraData = extraData - } - - // Add inputs to the transaction and do signing, validation, and broadcast - // depending on what the user requested. - totalInputt, spendAmountt, changeAmountt, feeNanoss, err = - fes.blockchain.AddInputsAndChangeToTransaction( - txnn, requestData.MinFeeRateNanosPerKB, fes.backendServer.GetMempool()) - if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("SendDeSo: Error processing transaction: %v", err)) - return - } - } + txnn, totalInputt, spendAmountt, changeAmountt, feeNanoss, err := fes.CreateSendDesoTxn( + requestData.AmountNanos, + senderPkBytes, + recipientPkBytes, + extraData, + requestData.MinFeeRateNanosPerKB, + additionalOutputs) // Sanity check that the input is equal to: // (spend amount + change amount + fees) @@ -2924,28 +2969,21 @@ type DAOCoinLimitOrderCreationRequest struct { OptionalPrecedingTransactions []*lib.MsgDeSoTxn `safeForLogging:"true"` } -// CreateDAOCoinLimitOrder Constructs a transaction that creates a DAO coin limit order for the specified -// DAO coin pair, price, quantity, operation type, and fill type -func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http.Request) { - decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) - requestData := DAOCoinLimitOrderCreationRequest{} - - if err := decoder.Decode(&requestData); err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: Problem parsing request body: %v", err)) - return - } - +func (fes *APIServer) createDaoCoinLimitOrderHelper( + requestData *DAOCoinLimitOrderCreationRequest, +) ( + _res *DAOCoinLimitOrderResponse, + _err error, +) { // Basic validation that we have a transactor if requestData.TransactorPublicKeyBase58Check == "" { - _AddBadRequestError(ww, "CreateDAOCoinLimitOrder: must provide a TransactorPublicKeyBase58Check") - return + return nil, errors.New("CreateDAOCoinLimitOrder: must provide a TransactorPublicKeyBase58Check") } // Validate operation type operationType, err := orderOperationTypeToUint64(requestData.OperationType) if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinLimitOrder: %v", err) } // Parse and validate fill type; for backwards compatibility, default the empty string to GoodTillCancelled @@ -2953,8 +2991,7 @@ func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http. if requestData.FillType != "" { fillType, err = orderFillTypeToUint64(requestData.FillType) if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinLimitOrder: %v", err) } } @@ -2980,8 +3017,7 @@ func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http. ) } if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinLimitOrder: %v", err) } // Parse and validated quantity @@ -3006,8 +3042,7 @@ func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http. ) } if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinLimitOrder: %v", err) } utxoView, err := lib.GetAugmentedUniversalViewWithAdditionalTransactions( @@ -3015,8 +3050,7 @@ func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http. requestData.OptionalPrecedingTransactions, ) if err != nil { - _AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: problem fetching utxoView: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinMarketOrder: problem fetching utxoView: %v", err) } // Decode and validate the buying / selling coin public keys @@ -3025,8 +3059,7 @@ func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http. requestData.SellingDAOCoinCreatorPublicKeyBase58Check, ) if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinLimitOrder: %v", err) } // Validate transactor has sufficient selling coins. @@ -3039,8 +3072,7 @@ func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http. quantityToFillInBaseUnits, ) if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinLimitOrder: %v", err) } // Validate any transfer restrictions on buying the DAO coin. @@ -3048,8 +3080,7 @@ func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http. requestData.TransactorPublicKeyBase58Check, requestData.BuyingDAOCoinCreatorPublicKeyBase58Check) if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinLimitOrder: %v", err) } // Create order. @@ -3067,8 +3098,7 @@ func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http. requestData.TransactionFees, ) if err != nil { - _AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinLimitOrder: %v", err) } res.SimulatedExecutionResult, err = fes.getDAOCoinLimitOrderSimulatedExecutionResult( @@ -3079,7 +3109,26 @@ func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http. res.Transaction, ) if err != nil { - _AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err)) + return nil, errors.Errorf("CreateDAOCoinLimitOrder: %v", err) + } + + return res, nil +} + +// CreateDAOCoinLimitOrder Constructs a transaction that creates a DAO coin limit order for the specified +// DAO coin pair, price, quantity, operation type, and fill type +func (fes *APIServer) CreateDAOCoinLimitOrder(ww http.ResponseWriter, req *http.Request) { + decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) + requestData := DAOCoinLimitOrderCreationRequest{} + + if err := decoder.Decode(&requestData); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: Problem parsing request body: %v", err)) + return + } + + res, err := fes.createDaoCoinLimitOrderHelper(&requestData) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinLimitOrder: %v", err)) return } @@ -3119,26 +3168,21 @@ type DAOCoinMarketOrderCreationRequest struct { OptionalPrecedingTransactions []*lib.MsgDeSoTxn `safeForLogging:"true"` } -func (fes *APIServer) CreateDAOCoinMarketOrder(ww http.ResponseWriter, req *http.Request) { - decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) - requestData := DAOCoinMarketOrderCreationRequest{} - - if err := decoder.Decode(&requestData); err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: Problem parsing request body: %v", err)) - return - } - +func (fes *APIServer) createDaoCoinMarketOrderHelper( + requestData *DAOCoinMarketOrderCreationRequest, +) ( + _res *DAOCoinLimitOrderResponse, + _err error, +) { // Basic validation that we have a transactor if requestData.TransactorPublicKeyBase58Check == "" { - _AddBadRequestError(ww, "CreateDAOCoinMarketOrder: must provide a TransactorPublicKeyBase58Check") - return + return nil, errors.New("CreateDAOCoinMarketOrder: must provide a TransactorPublicKeyBase58Check") } // Validate operation type operationType, err := orderOperationTypeToUint64(requestData.OperationType) if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinMarketOrder: %v", err) } // Validate and convert quantity to base units @@ -3166,22 +3210,16 @@ func (fes *APIServer) CreateDAOCoinMarketOrder(ww http.ResponseWriter, req *http } if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinMarketOrder: %v", err) } // Validate fill type fillType, err := orderFillTypeToUint64(requestData.FillType) if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinMarketOrder: %v", err) } if fillType == lib.DAOCoinLimitOrderFillTypeGoodTillCancelled { - _AddBadRequestError( - ww, - fmt.Sprintf("CreateDAOCoinMarketOrder: %v fill type not supported for market orders", requestData.FillType), - ) - return + return nil, errors.New("CreateDAOCoinMarketOrder: GoodTillCancelled fill type not supported for market orders") } // Validate any transfer restrictions on buying the DAO coin. @@ -3189,8 +3227,7 @@ func (fes *APIServer) CreateDAOCoinMarketOrder(ww http.ResponseWriter, req *http requestData.TransactorPublicKeyBase58Check, requestData.BuyingDAOCoinCreatorPublicKeyBase58Check) if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinMarketOrder: %v", err) } utxoView, err := lib.GetAugmentedUniversalViewWithAdditionalTransactions( @@ -3198,8 +3235,7 @@ func (fes *APIServer) CreateDAOCoinMarketOrder(ww http.ResponseWriter, req *http requestData.OptionalPrecedingTransactions, ) if err != nil { - _AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: problem fetching utxoView: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinMarketOrder: problem fetching utxoView: %v", err) } // Decode and validate the buying / selling coin public keys @@ -3208,8 +3244,7 @@ func (fes *APIServer) CreateDAOCoinMarketOrder(ww http.ResponseWriter, req *http requestData.SellingDAOCoinCreatorPublicKeyBase58Check, ) if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinMarketOrder: %v", err) } // override the initial value and explicitly set to 0 for clarity @@ -3229,8 +3264,7 @@ func (fes *APIServer) CreateDAOCoinMarketOrder(ww http.ResponseWriter, req *http requestData.TransactionFees, ) if err != nil { - _AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err)) - return + return nil, errors.Errorf("CreateDAOCoinMarketOrder: %v", err) } res.SimulatedExecutionResult, err = fes.getDAOCoinLimitOrderSimulatedExecutionResult( @@ -3241,7 +3275,23 @@ func (fes *APIServer) CreateDAOCoinMarketOrder(ww http.ResponseWriter, req *http res.Transaction, ) if err != nil { - _AddInternalServerError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err)) + return nil, errors.Errorf("CreateDAOCoinMarketOrder: %v", err) + } + return res, nil +} + +func (fes *APIServer) CreateDAOCoinMarketOrder(ww http.ResponseWriter, req *http.Request) { + decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) + requestData := DAOCoinMarketOrderCreationRequest{} + + if err := decoder.Decode(&requestData); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: Problem parsing request body: %v", err)) + return + } + + res, err := fes.createDaoCoinMarketOrderHelper(&requestData) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CreateDAOCoinMarketOrder: %v", err)) return } diff --git a/routes/user.go b/routes/user.go index 23d9fe72..bf843676 100644 --- a/routes/user.go +++ b/routes/user.go @@ -1534,11 +1534,6 @@ func (fes *APIServer) GetTokenBalancesForPublicKey(ww http.ResponseWriter, req * _AddBadRequestError(ww, fmt.Sprintf("GetTokenBalancesForPublicKey: Missing UserPublicKey")) return } - userPublicKeyBytes, _, err := lib.Base58CheckDecode(requestData.UserPublicKey) - if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("GetTokenBalancesForPublicKey: Problem decoding user public key: %v", err)) - return - } if len(requestData.CreatorPublicKeys) == 0 { _AddBadRequestError(ww, fmt.Sprintf("GetTokenBalancesForPublicKey: Missing CreatorPublicKeys")) return @@ -1546,41 +1541,21 @@ func (fes *APIServer) GetTokenBalancesForPublicKey(ww http.ResponseWriter, req * balancesMap := make(map[string]*SimpleTokenBalanceResponse) for _, creatorPublicKeyStr := range requestData.CreatorPublicKeys { - // Deso is a special case - if IsDesoPkid(creatorPublicKeyStr) { - desoNanos, err := utxoView.GetDeSoBalanceNanosForPublicKey(userPublicKeyBytes) - if err != nil { - _AddBadRequestError(ww, fmt.Sprintf("GetTokenBalancesForPublicKey: Problem getting DESO balance: %v", err)) - return - } - // When we're dealing with DESO, we use whatever identifier they passed - // in as the key. This is the most convenient thing to do for the caller. - // If we instead always returned DESO as the key then they would have to - // accommodate that, which would be annoying. - balancesMap[creatorPublicKeyStr] = &SimpleTokenBalanceResponse{ - UserPublicKeyBase58Check: requestData.UserPublicKey, - CreatorPublicKeyBase58Check: creatorPublicKeyStr, - BalanceBaseUnits: strconv.FormatUint(desoNanos, 10), - } - continue - } - creatorPkBytes, _, err := lib.Base58CheckDecode(creatorPublicKeyStr) + + balance, err := fes.getTransactorDesoOrDaoCoinBalance( + utxoView, + requestData.UserPublicKey, + creatorPublicKeyStr) if err != nil { _AddBadRequestError(ww, fmt.Sprintf( - "GetTokenBalancesForPublicKey: Problem decoding creator public key: %v", err)) + "GetTokenBalancesForPublicKey: Problem getting balance for user %v and creator %v: %v", + requestData.UserPublicKey, creatorPublicKeyStr, err)) return } - - balanceEntry, _, _ := utxoView.GetBalanceEntryForHODLerPubKeyAndCreatorPubKey( - userPublicKeyBytes, creatorPkBytes, true) - if balanceEntry == nil || balanceEntry.IsDeleted() { - balanceEntry = &lib.BalanceEntry{} - } - // Convert balanceEntry uint256 to string balancesMap[creatorPublicKeyStr] = &SimpleTokenBalanceResponse{ UserPublicKeyBase58Check: requestData.UserPublicKey, CreatorPublicKeyBase58Check: creatorPublicKeyStr, - BalanceBaseUnits: balanceEntry.BalanceNanos.String(), + BalanceBaseUnits: balance.ToBig().Text(10), } } diff --git a/scripts/global_params/update_global_params.go b/scripts/global_params/update_global_params.go index eb387c78..6e16c1af 100644 --- a/scripts/global_params/update_global_params.go +++ b/scripts/global_params/update_global_params.go @@ -5,12 +5,13 @@ import ( "encoding/hex" "encoding/json" "fmt" - "github.com/golang-jwt/jwt/v4" "io/ioutil" "net/http" "reflect" - "github.com/btcsuite/btcd/btcec/v2" + "github.com/golang-jwt/jwt/v4" + + "github.com/btcsuite/btcd/btcec" "github.com/deso-protocol/backend/routes" "github.com/deso-protocol/core/lib" "github.com/pkg/errors"