From 51306392b7b3f673a5f044124bd4d53e0ecc1752 Mon Sep 17 00:00:00 2001 From: diamondhands0 <81935176+diamondhands0@users.noreply.github.com> Date: Fri, 12 Jul 2024 19:43:56 -0700 Subject: [PATCH] New endpoint to support trading fees on limit orders for Openfund and Focus (#640) * Test backend CI against core with relic dependency. * Checkout specific branch just for testing purposes. * Update test Dockerfile. * Fix failing build stages. * Add new deps. * Allow ParamUpdater to update PoS consensus params (#499) * Allow ParamUpdater to update PoS GlobalParams. * Add epoch number to get pos params call. * Change core branch in test Dockerfile. * Add test for updating global params. * Fix bugs in testing updating global params. * Checkout correct core branch. * Change core branch. * Mf/add validator registration endpoints (#500) * Add validator registration endpoints. * Allow ExtraData to be logged. * Add relic dependency to prod dockerfile. * Mf/add get validator by public key endpoint (#501) * Add get validator by PublicKey route. * Address PR review feedback. * Rename VotingPublicKeySignature to VotingAuthorization. (#502) * Rename VotingPublicKeySignature to VotingAuthorization. * Checkout corresponding core branch. * Change core branch in test CI. * Refactor merging GlobalParamsEntry defaults. (#503) * Refactor merging GlobalParamsEntry defaults. * Change core branch in test dockerfile. * Update test.Dockerfile core branch. (#504) * Initial backend updates to conform to new function signatures in core (#512) Co-authored-by: Lazy Nina <> * Update backend to use BackendMempool interface instead of DeSoMempool (#513) Co-authored-by: Lazy Nina <> * Add fee estimator arg to all txn construction calls (#514) Co-authored-by: Lazy Nina <> * merge main into feature pos (#519) * [stable] Release 3.4.4 * add node version endpoint (#505) Co-authored-by: Lazy Nina <> * [stable] Release 3.4.5 * hotfix to exchange test * Add captcha verification (#509) * Updates to captcha verification * Updates to backend * Updates to captcha verify * Update captcha verification * Cleanup logs * Add routes to store reward amount in global state, track usage via data dog * Update verify captcha validation ordering, add back comp profile config bool * Update badger sync settings to optimize memory usage during hypersync (#506) * Update hypersync to use default badger settings, switch to performance settings once hypersync completes * Update test dockerfile to accept core branch name as parameter * Blank commit to trigger build * ln/fix-transaction-info-mempool (#510) Co-authored-by: Lazy Nina <> * ln/no-comp-when-0-create-profile-fee (#511) Co-authored-by: Lazy Nina <> * Empty commit to trigger build (#515) * Add extra data to basic transfer and diamond txn construction endpoints (#516) * trigger build (#517) Co-authored-by: Lazy Nina <> * trigger build * Add RWLock around AllCountryLevelSignUpBonuses (#518) Co-authored-by: Lazy Nina <> --------- Co-authored-by: Lazy Nina <> Co-authored-by: superzordon <88362450+superzordon@users.noreply.github.com> * Remove fee estimator from txn construction calls (#520) Co-authored-by: Lazy Nina <> * Fix compilation errors (#521) Co-authored-by: Lazy Nina <> * Fix validator test (#522) Co-authored-by: Lazy Nina <> * Add DelegatedStakeCommissionBasisPoints to RegisterAsValidator txn construction endpoint (#523) Co-authored-by: Lazy Nina <> * Add stake, unstake, and unlock stake txn construction endpoints (#524) * Add stake, unstake, and unlock stake txn construction endpoints * Add stake, unstake, and unlock stake txn construction endpoints --------- Co-authored-by: Lazy Nina <> * Add GET endpoints for stake and locked stake entries (#525) Co-authored-by: Lazy Nina <> * Add spending limits backend support for stake, unstake, unlock stake (#529) * Update Block Header Timestamps to int64 (#535) * Fix Backend To Run With Regtest PoS Node (#536) * Add txn construction and get endpoints for lockups (#526) * Add spending limits backend support for stake, unstake, unlock stake * Add txn construction and get endpoints for lockups * Add additional sanity checks to lockup endpoint. * Add txn construction and get endpoints for lockups * Add additional sanity checks to lockup endpoint. * Remove redundant profile entry response from LockedBalanceEntryResponse. * Add proper timestamp to simulateSubmitTransaction. * Apply suggestions from code review --------- Co-authored-by: Lazy Nina <> Co-authored-by: Jon Pollock * Add Unjail Validator endpoint (#532) * feature/pos-syncing-and-steady-state (#537) * Regtest PoS Validator Support * Fix n0_test script * Fix compile error * Fix CI (#540) * Only set public keys if not the zero pkid (#533) * Level 2 glog for price fetching logic (#541) * Remove datadir deletion from n0_test * Update miner to pass params into RecomputeBlockRewardWithBlockRewardOutputPublicKey (#542) * feature/pos-networking-and-syncing (#556) * Fix Compile Errors For Networking (#543) * Fix broken unit test (#546) * Add endpoint to get fee rate and block height for local txn construction (#544) * Fix NetworkManager Access (#550) * Reduce logging (#547) * Validator Registration Script (#552) * Validator Registration Script * Fix validator domain * Update flow crypto dependency (#554) --------- Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> * Update usages of EnumerateKeysForPrefix (#559) * Remove relic tag from validator test (#561) * Allow orphans in block index (#567) * Fix max block size logic (#568) * Empty commit to trigger CI * Upgrade Badger version (#566) * Fix Update global params tests (#571) * trigger build (#572) * trigger build (#573) * trigger build (#574) * Empty commit to trigger CI * bls pub key enhancements build (#576) * allow txn relay in needs blocks build (#577) * fix txn relay pos check build (#578) * Empty commit to trigger CI * Fix backend CI * Use GetCurrentGlobalParamsEntry instead of GlobalParamsEntry (#580) * Empty commit for CI * Empty commit for CI * Atomic Transaction Support for Backend (#560) * Update usage of CreateAtomicTxnsWrapper (#581) * Add a Preceding Transaction Field to Transaction Endpoints. (#570) * Simple CreateAtomicTxnsWrapper scaffolding. * Update method on CreateAtomicTxnsWrapper. * Use lib.GetAugmentedUniversalViewWithAdditionalTransactions for UpdateProfile. * Use MaxTxnSizeBytesPoS for atomic transaction construction. * Use updated lib.GetAugmentedUniversalViewWithAdditionalTransactions parameters. * Gate preceding transactions middleware on POST requests. * Use anonymous struct to simplify preceding transaction middleware. * Update stateful transaction endpoints to use GetAugmentedUniversalViewWithAdditionalTransactions. * Simple CreateAtomicTxnsWrapper scaffolding. * Update method on CreateAtomicTxnsWrapper. * Use MaxTxnSizeBytesPoS for atomic transaction construction. * Fix MaxTxnSizeBytes bug --------- Co-authored-by: diamondhands Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> * Add all new global params attributes to update global params txn construction endpoint (#583) * Trigger CI * Add get committed tip info endpoint (#582) * Trigger CI * Empty commit for CI * Update node info endpoint to return info about connections to validators (#586) * Upgrade go to 1.22 (#585) * Upgrade deps (#569) * Empty commit for CI * Fix remote node to response (#587) * trigger build 04-11-24 (#588) * Empty commit to trigger CI * Block until read only view regenerates in afterProcessSubmitPostTransaction (#590) * Add LatestView to CheckpointBlockInfo endpoint (#589) * Empty trigger for CI * Empty commit to trigger CI * Empty commit to trigger CI * Update usage of EstimateFeeRate (#592) * Submit atomic transactions endpoint. (#594) * Simple CreateAtomicTxnsWrapper scaffolding. * Update method on CreateAtomicTxnsWrapper. * Use MaxTxnSizeBytesPoS for atomic transaction construction. * Subsidized Update Profile Scaffolding * Simple CreateAtomicTxnsWrapper scaffolding. * Update method on CreateAtomicTxnsWrapper. * Use MaxTxnSizeBytesPoS for atomic transaction construction. * Fix MaxTxnSizeBytes bug * Implemented Subsidized Update Profile. * Add an endpoint for easily submitting incomplete atomic transactions. * Fix submit-atomic-transaction by using incomplete atomic hex. * Use mempool is update profile subsidization. * Remove subsidized update profile. --------- Co-authored-by: diamondhands * Empty commit to trigger CI * Make utxo ops nil on APITransactionToResponse for inner txns (#595) * Empty commit to trigger CI * Empty commit to CI * Add Script To Make Global Params Changes on Regtest Node (#597) * Add Script To Make Global Params Changes on Regtest Node * Remove public key * trigger build 041924 (#599) * Fix update_glboal_params package * Support MinFeeRateNanosPerKB in atomic txn construction (#602) * update call site for EstimateFeeRate (#603) * trigger build 042324 * trigger build 042324 * Gracefully Handle RemoteNode Response When Peer Is Not a Registered Validator (#604) * Empty commit to trigger CI * Empty commit to trigger CI * Gracefully Handle Nil Peer in RemoteNodeToResponse (#605) * Empty commit to trigger CI * Fix panic in get block template (#606) * Empty commit to trigger CI * Fix Failing Unit Tests (#607) * Add simple status check for node to backend (#608) * wip * wip * Upgrades to support DeSo AMMs and CCV2 (#609) This PR contains several SAFE upgrades to backend to support the various things we need for DeSo AMMs: - Add a `TxnStatus` field that can be provided as an argument to `GetDAOCoinLimitOrders`, `GetTxn`, and `GetTransactorDAOCoinLimitOrders`. This param allows the caller to either consider *unconfirmed txns*, which was the existing behavior before the introduction of this field, or to only consider *confirmed* txns. The latter is what we need for the AMM since we only want to be reacting to things that are finalized. Note that this change is SAFE because the default value is TxnStatusUnconfirmed, which was the pre-existing behavior. If you don't pass the argument, as none of the existing callers do, you will get the exact same result. It's only if you pass TxnStatusConfirmed, as the AMM code does, that you will get a different result than before. - Make it so that you can provide a list of OrderIds to `GetDAOCoinLimitOrdersRequest` and get back the ones that are currently on the book. Needed for the main loop of the AMM. - Introduce a new function called `IsDesoPkid` that flexibly allows the caller of backend endpoints to specify any of {"DESO", MiannetZeroPkidBase58Check, TestnetZeroPkidBase58Check} and get the same behavior. This change is needed because some things rely on using a ZeroPkid while other things rely on the "DESO" string being passed. This change is SAFE because it makes these functions LESS restrictive, and all pre-existing code that calls the endpoints with "DESO" are unaffected. - Allow for filtering by buying/selling pkid in `GetTransactorDAOCoinLimitOrders` . This is needed for the main loop of the AMM. This change is SAFE because the params are optional, and leaving them out gives you the exact same behavior as before. * Add GetDaoCaoinLimitOrdersById (#610) * Empty commit to trigger CI * empty commit to trigger build * Get JWT for update global params script (#615) * Empty commit to trigger CI * fix update global params scripts (#616) * empty commit to trigger build * Add GetCurrentEpochProgress API Endpoint to Return Leader Schedule and Current View (#617) * Add GetCurrentEpochProgress API Endpoint to Return Leader Schedule and Current View * Use currentView value from consensus * Empty Commit to Trigger CI * Update checksum script (#618) * Empty commit to trigger CI * Add Uint256Hex struct to hex encode old uint256 usages (#620) * Fix unmarshal nil pointer (#621) * Fix CheckPrecedingTransactions (#623) * Empty commit to trigger CI * Empty commit to trigger build * Add simple balance endpoint (for amm usage) (#628) * wip * Fix CheckPrecedingTransactions (#623) * Rename vars --------- Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> * Downgrade uint256 to 1.2.3 (#627) * Revert "Upgrade deps (#569)" (#630) This reverts commit 9f108080ed8b13d8b0f7907539fc942e96fb32f3. * Revert "Upgrade go to 1.22 (#585)" (#634) This reverts commit 53adfeed13af6121f11d8758914c0d7899654a90. * Revert Badger version from v4 to v3 (#636) * Empty commit to trigger CI * Empty commit to trigger build (#638) * Add blockheight check before GetSpendableUtxos (#639) * wip * wip * Bunch of upgrades * fix revert version errors * Incorporate core changes * fix nit * Fix deps and minor bug fixes * fix test.Dockerfile * Trigger build * Fix usd price computation in market order and add price endpoint * Fix bug around quantity currency * Fix txindex bug * Trigger rebuild * fix gettxn * review comments * Review fixes * Patch last pkid issue --------- Co-authored-by: mattfoley8 Co-authored-by: Matt Foley <100429827+mattfoley8@users.noreply.github.com> Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> Co-authored-by: superzordon <88362450+superzordon@users.noreply.github.com> Co-authored-by: tholonious <99746187+tholonious@users.noreply.github.com> Co-authored-by: Jon Pollock Co-authored-by: iamsofonias Co-authored-by: Jon Pollock <135658176+poolcoke@users.noreply.github.com> Co-authored-by: Lazy Nina Co-authored-by: superzordon --- go.mod | 2 +- routes/dao_coin_exchange.go | 44 +- routes/dao_coin_exchange_with_fees.go | 1616 +++++++++++++++++ routes/extra_data_utils.go | 5 +- routes/server.go | 34 + routes/transaction.go | 298 +-- routes/user.go | 41 +- scripts/global_params/update_global_params.go | 5 +- 8 files changed, 1882 insertions(+), 163 deletions(-) create mode 100644 routes/dao_coin_exchange_with_fees.go 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"