From 734d12efcfcf2d0fb2cb61f396a31010903a8297 Mon Sep 17 00:00:00 2001 From: Lazy Nina <81658138+lazynina@users.noreply.github.com> Date: Fri, 2 Feb 2024 15:41:14 -0500 Subject: [PATCH] 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 --- routes/lockups.go | 733 ++++++++++++++++++++++++++++++++++++++++++ routes/server.go | 50 +++ routes/stake.go | 1 + routes/transaction.go | 42 ++- routes/user.go | 11 +- 5 files changed, 832 insertions(+), 5 deletions(-) create mode 100644 routes/lockups.go diff --git a/routes/lockups.go b/routes/lockups.go new file mode 100644 index 00000000..45d7f08f --- /dev/null +++ b/routes/lockups.go @@ -0,0 +1,733 @@ +package routes + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "github.com/deso-protocol/core/collections" + "github.com/deso-protocol/core/lib" + "github.com/gorilla/mux" + "github.com/holiman/uint256" + "io" + "net/http" + "reflect" + "time" +) + +type CumulativeLockedBalanceEntryResponse struct { + HODLerPublicKeyBase58Check string + ProfilePublicKeyBase58Check string + TotalLockedBaseUnits uint256.Int + UnlockableBaseUnits uint256.Int + UnvestedLockedBalanceEntries []*LockedBalanceEntryResponse + VestedLockedBalanceEntries []*LockedBalanceEntryResponse + ProfileEntryResponse *ProfileEntryResponse +} + +type LockedBalanceEntryResponse struct { + HODLerPublicKeyBase58Check string + ProfilePublicKeyBase58Check string + UnlockTimestampNanoSecs int64 + VestingEndTimestampNanoSecs int64 + BalanceBaseUnits uint256.Int +} + +func (fes *APIServer) _lockedBalanceEntryToResponse( + lockedBalanceEntry *lib.LockedBalanceEntry, utxoView *lib.UtxoView, params *lib.DeSoParams, +) *LockedBalanceEntryResponse { + hodlerPublicKey := utxoView.GetPublicKeyForPKID(lockedBalanceEntry.HODLerPKID) + profilePublicKey := utxoView.GetPublicKeyForPKID(lockedBalanceEntry.ProfilePKID) + return &LockedBalanceEntryResponse{ + HODLerPublicKeyBase58Check: lib.PkToString(hodlerPublicKey, params), + ProfilePublicKeyBase58Check: lib.PkToString(profilePublicKey, params), + UnlockTimestampNanoSecs: lockedBalanceEntry.UnlockTimestampNanoSecs, + VestingEndTimestampNanoSecs: lockedBalanceEntry.VestingEndTimestampNanoSecs, + BalanceBaseUnits: lockedBalanceEntry.BalanceBaseUnits, + } +} + +type LockupYieldCurvePointResponse struct { + ProfilePublicKeyBase58Check string + LockupDurationNanoSecs int64 + LockupYieldAPYBasisPoints uint64 + ProfileEntryResponse *ProfileEntryResponse +} + +func (fes *APIServer) _lockupYieldCurvePointToResponse( + lockupYieldCurvePoint *lib.LockupYieldCurvePoint, utxoView *lib.UtxoView, params *lib.DeSoParams, +) *LockupYieldCurvePointResponse { + profilePublicKey := utxoView.GetPublicKeyForPKID(lockupYieldCurvePoint.ProfilePKID) + profileEntry := utxoView.GetProfileEntryForPKID(lockupYieldCurvePoint.ProfilePKID) + profileEntryResponse := fes._profileEntryToResponse(profileEntry, utxoView) + return &LockupYieldCurvePointResponse{ + ProfilePublicKeyBase58Check: lib.PkToString(profilePublicKey, params), + LockupDurationNanoSecs: lockupYieldCurvePoint.LockupDurationNanoSecs, + LockupYieldAPYBasisPoints: lockupYieldCurvePoint.LockupYieldAPYBasisPoints, + ProfileEntryResponse: profileEntryResponse, + } +} + +type CoinLockupRequest struct { + TransactorPublicKeyBase58Check string `safeForLogging:"true"` + ProfilePublicKeyBase58Check string `safeForLogging:"true"` + RecipientPublicKeyBase58Check string `safeForLogging:"true"` + UnlockTimestampNanoSecs int64 `safeForLogging:"true"` + VestingEndTimestampNanoSecs int64 `safeForLogging:"true"` + LockupAmountBaseUnits *uint256.Int `safeForLogging:"true"` + ExtraData map[string]string `safeForLogging:"true"` + MinFeeRateNanosPerKB uint64 `safeForLogging:"true"` + TransactionFees []TransactionFee `safeForLogging:"true"` +} + +type UpdateCoinLockupParamsRequest struct { + TransactorPublicKeyBase58Check string `safeForLogging:"true"` + LockupYieldDurationNanoSecs int64 `safeForLogging:"true"` + LockupYieldAPYBasisPoints uint64 `safeForLogging:"true"` + RemoveYieldCurvePoint bool `safeForLogging:"true"` + NewLockupTransferRestrictions bool `safeForLogging:"true"` + LockupTransferRestrictionStatus TransferRestrictionStatusString `safeForLogging:"true"` + ExtraData map[string]string `safeForLogging:"true"` + MinFeeRateNanosPerKB uint64 `safeForLogging:"true"` + TransactionFees []TransactionFee `safeForLogging:"true"` +} + +type CoinLockupTransferRequest struct { + TransactorPublicKeyBase58Check string `safeForLogging:"true"` + ProfilePublicKeyBase58Check string `safeForLogging:"true"` + RecipientPublicKeyBase58Check string `safeForLogging:"true"` + UnlockTimestampNanoSecs int64 `safeForLogging:"true"` + LockedCoinsToTransferBaseUnits *uint256.Int `safeForLogging:"true"` + ExtraData map[string]string `safeForLogging:"true"` + MinFeeRateNanosPerKB uint64 `safeForLogging:"true"` + TransactionFees []TransactionFee `safeForLogging:"true"` +} + +type CoinUnlockRequest struct { + TransactorPublicKeyBase58Check string `safeForLogging:"true"` + ProfilePublicKeyBase58Check string `safeForLogging:"true"` + ExtraData map[string]string `safeForLogging:"true"` + MinFeeRateNanosPerKB uint64 `safeForLogging:"true"` + TransactionFees []TransactionFee `safeForLogging:"true"` +} + +type CoinLockResponse struct { + SpendAmountNanos uint64 + TotalInputNanos uint64 + ChangeAmountNanos uint64 + FeeNanos uint64 + Transaction *lib.MsgDeSoTxn + TransactionHex string + TxnHashHex string +} + +func (fes *APIServer) CoinLockup(ww http.ResponseWriter, req *http.Request) { + // Decode request body. + decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) + requestData := CoinLockupRequest{} + if err := decoder.Decode(&requestData); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockup: Problem parsing request body: %v", err)) + return + } + + // Convert TransactorPublicKeyBase58Check to TransactorPublicKeyBytes + if requestData.TransactorPublicKeyBase58Check == "" { + _AddBadRequestError(ww, fmt.Sprint("CoinLockup: TransactorPublicKeyBase58Check is required")) + return + } + transactorPublicKeyBytes, _, err := lib.Base58CheckDecode(requestData.TransactorPublicKeyBase58Check) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockup: Problem decoding TransactorPublicKeyBase58Check %s: %v", + requestData.TransactorPublicKeyBase58Check, err)) + return + } + + // Convert ProfilePublicKeyBase58Check to ProfilePublicKeyBytes + if requestData.ProfilePublicKeyBase58Check == "" { + _AddBadRequestError(ww, fmt.Sprint("CoinLockup: ProfilePublicKeyBase58Check is required")) + return + } + profilePublicKeyBytes, _, err := lib.Base58CheckDecode(requestData.ProfilePublicKeyBase58Check) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockup: Problem decoding ProfilePublicKeyBase58Check %s: %v", + requestData.ProfilePublicKeyBase58Check, err)) + return + + } + + // Convert RecipientPublicKeyBase58Check to RecipientPublicKeyBytes if it exists + var recipientPublicKeyBytes []byte + if requestData.RecipientPublicKeyBase58Check != "" { + recipientPublicKeyBytes, _, err = lib.Base58CheckDecode(requestData.RecipientPublicKeyBase58Check) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockup: Problem decoding RecipientPublicKeyBase58Check %s: %v", + requestData.RecipientPublicKeyBase58Check, err)) + return + } + } + + // Sanity check that the lockup appears to occur in the future. + currentTimestampNanoSecs := time.Now().UnixNano() + if requestData.UnlockTimestampNanoSecs < currentTimestampNanoSecs { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockup: The unlock timestamp cannot be in the past "+ + "(unlock timestamp: %d, current timestamp: %d)\n", + requestData.UnlockTimestampNanoSecs, currentTimestampNanoSecs)) + return + } + + // Sanity check that the vested lockup does not go into the past. + if requestData.UnlockTimestampNanoSecs > requestData.VestingEndTimestampNanoSecs { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockup: Vested lockups cannot vest into the past "+ + "(unlock timestamp: %d, vesting end timestamp: %d\n", + requestData.UnlockTimestampNanoSecs, requestData.VestingEndTimestampNanoSecs)) + return + } + + // Sanity check that the lockup request amount is non-zero. + if requestData.LockupAmountBaseUnits.IsZero() { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockup: Cannot lockup an amount of zero\n")) + return + } + + // Encode the extra data. + extraData, err := EncodeExtraDataMap(requestData.ExtraData) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockup: Problem encoding ExtraData: %v", err)) + return + } + + // Compute the additional transaction fees as specified + // by the request body and the node-level fees. + additionalOutputs, err := fes.getTransactionFee( + lib.TxnTypeCoinLockup, + transactorPublicKeyBytes, + requestData.TransactionFees, + ) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockup: specified TransactionFees are invalid: %v", err)) + return + } + + // Create transaction + txn, totalInput, changeAmount, fees, err := fes.blockchain.CreateCoinLockupTxn( + transactorPublicKeyBytes, + profilePublicKeyBytes, + recipientPublicKeyBytes, + requestData.UnlockTimestampNanoSecs, + requestData.VestingEndTimestampNanoSecs, + requestData.LockupAmountBaseUnits, + extraData, + requestData.MinFeeRateNanosPerKB, + fes.backendServer.GetMempool(), + additionalOutputs, + ) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockup: Problem creating txn: %v", err)) + return + } + + // Construct response. + txnBytes, err := txn.ToBytes(true) + if err != nil { + _AddInternalServerError(ww, fmt.Sprintf("CoinLockup: Problem serializing txn: %v", err)) + return + } + + res := CoinLockResponse{ + SpendAmountNanos: totalInput - changeAmount - fees, + 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 { + _AddInternalServerError(ww, fmt.Sprintf("CoinLockup: Problem encoding response as JSON: %v", err)) + return + } +} + +func (fes *APIServer) UpdateCoinLockupParams(ww http.ResponseWriter, req *http.Request) { + // Decode request body. + decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) + requestData := UpdateCoinLockupParamsRequest{} + if err := decoder.Decode(&requestData); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateCoinLockupParams: Problem parsing request body: %v", err)) + return + } + + // Convert TransactorPublicKeyBase58Check to TransactorPublicKeyBytes + if requestData.TransactorPublicKeyBase58Check == "" { + _AddBadRequestError(ww, fmt.Sprint("UpdateCoinLockupParams: TransactorPublicKeyBase58Check is required")) + return + } + transactorPublicKeyBytes, _, err := lib.Base58CheckDecode(requestData.TransactorPublicKeyBase58Check) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateCoinLockupParams: Problem decoding TransactorPublicKeyBase58Check %s: %v", + requestData.TransactorPublicKeyBase58Check, err)) + return + } + + var transferRestrictionStatus lib.TransferRestrictionStatus + if requestData.NewLockupTransferRestrictions { + switch requestData.LockupTransferRestrictionStatus { + case TransferRestrictionStatusStringUnrestricted: + transferRestrictionStatus = lib.TransferRestrictionStatusUnrestricted + case TransferRestrictionStatusStringProfileOwnerOnly: + transferRestrictionStatus = lib.TransferRestrictionStatusProfileOwnerOnly + case TransferRestrictionStatusStringDAOMembersOnly: + transferRestrictionStatus = lib.TransferRestrictionStatusDAOMembersOnly + case TransferRestrictionStatusStringPermanentlyUnrestricted: + transferRestrictionStatus = lib.TransferRestrictionStatusPermanentlyUnrestricted + default: + _AddBadRequestError(ww, fmt.Sprintf( + "UpdateCoinLockupParams: TransferRestrictionStatus \"%v\" not supported", + requestData.LockupTransferRestrictionStatus)) + return + } + } + + // Parse extra data. + extraData, err := EncodeExtraDataMap(requestData.ExtraData) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateCoinLockupParams: Problem encoding ExtraData: %v", err)) + return + } + + // Compute the additional transaction fees as specified + // by the request body and the node-level fees. + additionalOutputs, err := fes.getTransactionFee( + lib.TxnTypeUpdateCoinLockupParams, + transactorPublicKeyBytes, + requestData.TransactionFees, + ) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateCoinLockupParams: specified TransactionFees are invalid: %v", err)) + return + } + + // Create transaction + txn, totalInput, changeAmount, fees, err := fes.blockchain.CreateUpdateCoinLockupParamsTxn( + transactorPublicKeyBytes, + requestData.LockupYieldDurationNanoSecs, + requestData.LockupYieldAPYBasisPoints, + requestData.RemoveYieldCurvePoint, + requestData.NewLockupTransferRestrictions, + transferRestrictionStatus, + extraData, + requestData.MinFeeRateNanosPerKB, + fes.backendServer.GetMempool(), + additionalOutputs, + ) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("UpdateCoinLockupParams: Problem creating txn: %v", err)) + return + } + + // Construct response. + txnBytes, err := txn.ToBytes(true) + if err != nil { + _AddInternalServerError(ww, fmt.Sprintf("UpdateCoinLockupParams: Problem serializing txn: %v", err)) + return + } + + res := CoinLockResponse{ + SpendAmountNanos: totalInput - changeAmount - fees, + 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 { + _AddInternalServerError(ww, fmt.Sprintf("UpdateCoinLockupParams: Problem encoding response as JSON: %v", err)) + return + } +} + +func (fes *APIServer) CoinLockupTransfer(ww http.ResponseWriter, req *http.Request) { + // Decode request body. + decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) + requestData := CoinLockupTransferRequest{} + if err := decoder.Decode(&requestData); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockupTransfer: Problem parsing request body: %v", err)) + return + } + + // Convert TransactorPublicKeyBase58Check to TransactorPublicKeyBytes + if requestData.TransactorPublicKeyBase58Check == "" { + _AddBadRequestError(ww, fmt.Sprint("CoinLockupTransfer: TransactorPublicKeyBase58Check is required")) + return + } + transactorPublicKeyBytes, _, err := lib.Base58CheckDecode(requestData.TransactorPublicKeyBase58Check) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf( + "CoinLockupTransfer: Problem decoding TransactorPublicKeyBase58Check %s: %v", + requestData.TransactorPublicKeyBase58Check, err)) + return + } + + // Convert ProfilePublicKeyBase58Check to ProfilePublicKeyBytes + if requestData.ProfilePublicKeyBase58Check == "" { + _AddBadRequestError(ww, fmt.Sprint("CoinLockupTransfer: ProfilePublicKeyBase58Check is required")) + return + } + profilePublicKeyBytes, _, err := lib.Base58CheckDecode(requestData.ProfilePublicKeyBase58Check) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockupTransfer: Problem decoding ProfilePublicKeyBase58Check %s: %v", + requestData.ProfilePublicKeyBase58Check, err)) + return + } + + // Convert RecipientPublicKeyBase58Check to RecipientPublicKeyBytes + if requestData.RecipientPublicKeyBase58Check == "" { + _AddBadRequestError(ww, fmt.Sprint("CoinLockupTransfer: RecipientPublicKeyBase58Check is required")) + return + } + recipientPublicKeyBytes, _, err := lib.Base58CheckDecode(requestData.RecipientPublicKeyBase58Check) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockupTransfer: Problem decoding RecipientPublicKeyBase58Check %s: %v", + requestData.RecipientPublicKeyBase58Check, err)) + return + } + + // Check to ensure the recipient is different than the sender. + if reflect.DeepEqual(recipientPublicKeyBytes, transactorPublicKeyBytes) { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockupTransfer: Sender cannot be receiver of a transfer")) + return + } + + // Parse extra data. + extraData, err := EncodeExtraDataMap(requestData.ExtraData) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockupTransfer: Problem encoding ExtraData: %v", err)) + return + } + + // Compute the additional transaction fees as specified + // by the request body and the node-level fees. + additionalOutputs, err := fes.getTransactionFee( + lib.TxnTypeCoinLockupTransfer, + transactorPublicKeyBytes, + requestData.TransactionFees, + ) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockupTransfer: specified TransactionFees are invalid: %v", err)) + return + } + + // Create transaction + txn, totalInput, changeAmount, fees, err := fes.blockchain.CreateCoinLockupTransferTxn( + transactorPublicKeyBytes, + recipientPublicKeyBytes, + profilePublicKeyBytes, + requestData.UnlockTimestampNanoSecs, + requestData.LockedCoinsToTransferBaseUnits, + extraData, + requestData.MinFeeRateNanosPerKB, + fes.backendServer.GetMempool(), + additionalOutputs, + ) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinLockupTransfer: Problem creating txn: %v", err)) + return + } + + // Construct response. + txnBytes, err := txn.ToBytes(true) + if err != nil { + _AddInternalServerError(ww, fmt.Sprintf("CoinLockupTransfer: Problem serializing txn: %v", err)) + return + } + + res := CoinLockResponse{ + SpendAmountNanos: totalInput - changeAmount - fees, + 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 { + _AddInternalServerError(ww, fmt.Sprintf("CoinLockupTransfer: Problem encoding response as JSON: %v", err)) + return + } +} + +func (fes *APIServer) CoinUnlock(ww http.ResponseWriter, req *http.Request) { + // Decode request body. + decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes)) + requestData := CoinUnlockRequest{} + if err := decoder.Decode(&requestData); err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinUnlock: Problem parsing request body: %v", err)) + return + } + + // Convert TransactorPublicKeyBase58Check to TransactorPublicKeyBytes + if requestData.TransactorPublicKeyBase58Check == "" { + _AddBadRequestError(ww, fmt.Sprint("CoinUnlock: TransactorPublicKeyBase58Check is required")) + return + } + transactorPublicKeyBytes, _, err := lib.Base58CheckDecode(requestData.TransactorPublicKeyBase58Check) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinUnlock: Problem decoding TransactorPublicKeyBase58Check %s: %v", + requestData.TransactorPublicKeyBase58Check, err)) + return + } + + // Convert ProfilePublicKeyBase58Check to ProfilePublicKeyBytes + if requestData.ProfilePublicKeyBase58Check == "" { + _AddBadRequestError(ww, fmt.Sprint("CoinUnlock: ProfilePublicKeyBase58Check is required")) + return + } + profilePublicKeyBytes, _, err := lib.Base58CheckDecode(requestData.ProfilePublicKeyBase58Check) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinUnlock: Problem decoding ProfilePublicKeyBase58Check %s: %v", + requestData.ProfilePublicKeyBase58Check, err)) + return + } + + // Parse extra data. + extraData, err := EncodeExtraDataMap(requestData.ExtraData) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinUnlock: Problem encoding ExtraData: %v", err)) + return + } + + // Compute the additional transaction fees as specified + // by the request body and the node-level fees. + additionalOutputs, err := fes.getTransactionFee( + lib.TxnTypeCoinUnlock, + transactorPublicKeyBytes, + requestData.TransactionFees, + ) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinUnlock: specified TransactionFees are invalid: %v", err)) + return + } + + // Create transaction + txn, totalInput, changeAmount, fees, err := fes.blockchain.CreateCoinUnlockTxn( + transactorPublicKeyBytes, + profilePublicKeyBytes, + extraData, + requestData.MinFeeRateNanosPerKB, + fes.backendServer.GetMempool(), + additionalOutputs, + ) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("CoinUnlock: Problem creating txn: %v", err)) + return + } + + // Construct response. + txnBytes, err := txn.ToBytes(true) + if err != nil { + _AddInternalServerError(ww, fmt.Sprintf("CoinUnlock: Problem serializing txn: %v", err)) + return + } + + res := CoinLockResponse{ + SpendAmountNanos: totalInput - changeAmount - fees, + 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 { + _AddInternalServerError(ww, fmt.Sprintf("CoinUnlock: Problem encoding response as JSON: %v", err)) + return + } +} + +// GET lockup yield curve points for a profile by public key +func (fes *APIServer) LockedYieldCurvePoints(ww http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + publicKeyBase58Check := vars[publicKeyBase58CheckKey] + + if publicKeyBase58Check == "" { + _AddBadRequestError(ww, fmt.Sprintf("LockedYieldCurvePoints: PublicKeyBase58Check is required")) + return + } + + utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView() + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("LockedYieldCurvePoints: Problem getting utxoView: %v", err)) + return + } + + // Decode public key + pkid, err := fes.getPKIDFromPublicKeyBase58Check(utxoView, publicKeyBase58Check) + if err != nil || pkid == nil { + _AddBadRequestError(ww, fmt.Sprintf("LockedYieldCurvePoints: Problem decoding public key: %v", err)) + return + } + + // Get locked yield curve points + yieldCurvePointsMap, err := utxoView.GetAllYieldCurvePoints(pkid) + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("LockedYieldCurvePoints: Problem getting yield curve points: %v", err)) + return + } + + var allYieldCurvePoints []*LockupYieldCurvePointResponse + for _, yieldCurvePoint := range yieldCurvePointsMap { + if yieldCurvePoint.IsDeleted() { + continue + } + allYieldCurvePoints = append(allYieldCurvePoints, + fes._lockupYieldCurvePointToResponse(yieldCurvePoint, utxoView, fes.Params)) + } + + sortedYieldCurvePoints := collections.SortStable(allYieldCurvePoints, + func(ii *LockupYieldCurvePointResponse, jj *LockupYieldCurvePointResponse) bool { + return ii.LockupDurationNanoSecs < jj.LockupDurationNanoSecs + }) + + if err = json.NewEncoder(ww).Encode(sortedYieldCurvePoints); err != nil { + _AddInternalServerError(ww, fmt.Sprintf("LockedYieldCurvePoints: Problem encoding response as JSON: %v", err)) + return + } +} + +// GET all locked balance entries held by a HODLer public key +func (fes *APIServer) LockedBalanceEntries(ww http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + publicKeyBase58Check := vars[publicKeyBase58CheckKey] + + if publicKeyBase58Check == "" { + _AddBadRequestError(ww, fmt.Sprintf("LockedBalanceEntriesHeldByPublicKey: PublicKeyBase58Check is required")) + return + } + + // Create an augmented UTXO view to include uncomitted transactions. + utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView() + if err != nil { + _AddBadRequestError(ww, fmt.Sprintf("LockedBalanceEntriesHeldByPublicKey: Problem getting utxoView: %v", err)) + return + } + + // Decode public key + pkid, err := fes.getPKIDFromPublicKeyBase58Check(utxoView, publicKeyBase58Check) + if err != nil || pkid == nil { + _AddBadRequestError(ww, fmt.Sprintf("LockedBalanceEntriesHeldByPublicKey: Problem decoding public key: %v", err)) + return + } + + // Get all locked balance entries for a user. + lockedBalanceEntries, err := utxoView.GetAllLockedBalanceEntriesForHodlerPKID(pkid) + if err != nil { + _AddBadRequestError(ww, + fmt.Sprintf("LockedBalanceEntries: Problem getting locked balance entries: %v", err)) + return + } + + // Split the locked balance entries based on the creator. + creatorPKIDToCumulativeLockedBalanceEntryResponse := make(map[lib.PKID]*CumulativeLockedBalanceEntryResponse) + currentTimestampNanoSecs := time.Now().UnixNano() + for _, lockedBalanceEntry := range lockedBalanceEntries { + // Check if we need to initialize the cumulative response. + if _, exists := creatorPKIDToCumulativeLockedBalanceEntryResponse[*lockedBalanceEntry.ProfilePKID]; !exists { + hodlerPublicKey := utxoView.GetPublicKeyForPKID(lockedBalanceEntry.HODLerPKID) + profilePublicKey := utxoView.GetPublicKeyForPKID(lockedBalanceEntry.ProfilePKID) + profileEntry := utxoView.GetProfileEntryForPKID(lockedBalanceEntry.ProfilePKID) + profileEntryResponse := fes._profileEntryToResponse(profileEntry, utxoView) + + creatorPKIDToCumulativeLockedBalanceEntryResponse[*lockedBalanceEntry.ProfilePKID] = + &CumulativeLockedBalanceEntryResponse{ + HODLerPublicKeyBase58Check: lib.PkToString(hodlerPublicKey, fes.Params), + ProfilePublicKeyBase58Check: lib.PkToString(profilePublicKey, fes.Params), + TotalLockedBaseUnits: uint256.Int{}, + UnlockableBaseUnits: uint256.Int{}, + UnvestedLockedBalanceEntries: []*LockedBalanceEntryResponse{}, + VestedLockedBalanceEntries: []*LockedBalanceEntryResponse{}, + ProfileEntryResponse: profileEntryResponse, + } + } + + // Get the existing cumulative response. + cumulativeResponse := creatorPKIDToCumulativeLockedBalanceEntryResponse[*lockedBalanceEntry.ProfilePKID] + + // Update the total locked base units. + // NOTE: It's possible to create multiple locked balance entries that are impossible to unlock due to overflow. + // As such, if the addition triggers an overflow we will just ignore adding more and use the max Uint256. + var newTotalLockedBaseUnits *uint256.Int + if uint256.NewInt().Sub( + lib.MaxUint256, + &cumulativeResponse.TotalLockedBaseUnits).Lt(&lockedBalanceEntry.BalanceBaseUnits) { + newTotalLockedBaseUnits = lib.MaxUint256 + } else { + newTotalLockedBaseUnits = uint256.NewInt().Add( + &cumulativeResponse.TotalLockedBaseUnits, + &lockedBalanceEntry.BalanceBaseUnits) + } + + // Compute how much (if any) is unlockable in the give entry. + unlockableBaseUnitsFromEntry := uint256.NewInt() + newTotalUnlockableBaseUnits := uint256.NewInt() + if lockedBalanceEntry.UnlockTimestampNanoSecs < currentTimestampNanoSecs { + // Check if the locked balance entry is unvested or vested. + if lockedBalanceEntry.UnlockTimestampNanoSecs == lockedBalanceEntry.VestingEndTimestampNanoSecs { + unlockableBaseUnitsFromEntry = &lockedBalanceEntry.BalanceBaseUnits + } else { + unlockableBaseUnitsFromEntry, err = + lib.CalculateVestedEarnings(lockedBalanceEntry, currentTimestampNanoSecs) + if err != nil { + _AddBadRequestError(ww, + fmt.Sprintf("LockedBalanceEntries: Problem computing vested earnings: %v", err)) + return + } + } + } + if uint256.NewInt().Sub( + lib.MaxUint256, + &cumulativeResponse.UnlockableBaseUnits).Lt(unlockableBaseUnitsFromEntry) { + newTotalUnlockableBaseUnits = lib.MaxUint256 + } else { + newTotalUnlockableBaseUnits = uint256.NewInt().Add( + &cumulativeResponse.UnlockableBaseUnits, + unlockableBaseUnitsFromEntry) + } + + // Update the cumulative response. + cumulativeResponse.TotalLockedBaseUnits = *newTotalLockedBaseUnits + cumulativeResponse.UnlockableBaseUnits = *newTotalUnlockableBaseUnits + if lockedBalanceEntry.UnlockTimestampNanoSecs == lockedBalanceEntry.VestingEndTimestampNanoSecs { + cumulativeResponse.UnvestedLockedBalanceEntries = append( + cumulativeResponse.UnvestedLockedBalanceEntries, + fes._lockedBalanceEntryToResponse(lockedBalanceEntry, utxoView, fes.Params)) + } else { + cumulativeResponse.VestedLockedBalanceEntries = append( + cumulativeResponse.VestedLockedBalanceEntries, + fes._lockedBalanceEntryToResponse(lockedBalanceEntry, utxoView, fes.Params)) + } + } + + // Create a list of the cumulative locked balance entries and sort based on amount locked. + var cumulativeLockedBalanceEntryResponses []*CumulativeLockedBalanceEntryResponse + for _, cumulativeResponse := range creatorPKIDToCumulativeLockedBalanceEntryResponse { + cumulativeLockedBalanceEntryResponses = append( + cumulativeLockedBalanceEntryResponses, cumulativeResponse) + } + + // Sort the response based on the amount locked. + sortedCumulativeResponses := collections.SortStable(cumulativeLockedBalanceEntryResponses, + func(ii *CumulativeLockedBalanceEntryResponse, jj *CumulativeLockedBalanceEntryResponse) bool { + return ii.TotalLockedBaseUnits.Lt(&jj.TotalLockedBaseUnits) + }) + + // Encode and return the responses. + if err = json.NewEncoder(ww).Encode(sortedCumulativeResponses); err != nil { + _AddInternalServerError(ww, + fmt.Sprintf("LockedBalanceEntries: Problem encoding response as JSON: %v", err)) + return + } +} diff --git a/routes/server.go b/routes/server.go index 1d17ece5..77eddf69 100644 --- a/routes/server.go +++ b/routes/server.go @@ -322,6 +322,14 @@ const ( RoutePathUnstake = "/api/v0/unstake" RoutePathUnlockStake = "/api/v0/unlock-stake" RoutePathLockedStake = "/api/v0/locked-stake" + + // lockups.go + RoutePathCoinLockup = "/api/v0/coin-lockup" + RoutePathUpdateCoinLockupParams = "/api/v0/update-coin-lockup-params" + RoutePathCoinLockupTransfer = "/api/v0/coin-lockup-transfer" + RoutePathCoinUnlock = "/api/v0/coin-unlock" + RoutePathLockupYieldCurvePoints = "/api/v0/lockup-yield-curve-points" + RoutePathLockedBalanceEntries = "/api/v0/locked-balance-entries" ) // APIServer provides the interface between the blockchain and things like the @@ -1354,6 +1362,48 @@ func (fes *APIServer) NewRouter() *muxtrace.Router { fes.GetLockedStakesForValidatorAndStaker, PublicAccess, }, + { + "CoinLockup", + []string{"POST", "OPTIONS"}, + RoutePathCoinLockup, + fes.CoinLockup, + PublicAccess, + }, + { + "UpdateCoinLockupParams", + []string{"POST", "OPTIONS"}, + RoutePathUpdateCoinLockupParams, + fes.UpdateCoinLockupParams, + PublicAccess, + }, + { + "CoinLockupTransfer", + []string{"POST", "OPTIONS"}, + RoutePathCoinLockupTransfer, + fes.CoinLockupTransfer, + PublicAccess, + }, + { + "CoinUnlock", + []string{"POST", "OPTIONS"}, + RoutePathCoinUnlock, + fes.CoinUnlock, + PublicAccess, + }, + { + "LockedYieldCurvePoints", + []string{"GET"}, + RoutePathLockupYieldCurvePoints + "/" + makePublicKeyParamRegex(publicKeyBase58CheckKey), + fes.LockedYieldCurvePoints, + PublicAccess, + }, + { + "LockedBalanceEntries", + []string{"GET"}, + RoutePathLockedBalanceEntries + "/" + makePublicKeyParamRegex(publicKeyBase58CheckKey), + fes.LockedBalanceEntries, + PublicAccess, + }, // Jumio Routes { "JumioBegin", diff --git a/routes/stake.go b/routes/stake.go index 583280b2..891d9c74 100644 --- a/routes/stake.go +++ b/routes/stake.go @@ -109,6 +109,7 @@ const ( lockedAtEpochNumberKey = "lockedAtEpochNumber" startEpochNumberKey = "startEpochNumber" endEpochNumberKey = "endEpochNumber" + publicKeyBase58CheckKey = "publicKeyBase58Check" ) // Stake constructs a transaction that stakes a given amount of DeSo. diff --git a/routes/transaction.go b/routes/transaction.go index 070af111..797401ac 100644 --- a/routes/transaction.go +++ b/routes/transaction.go @@ -3249,6 +3249,13 @@ type UnlockStakeLimitMapItem struct { OpCount uint64 } +type LockupLimitMapItem struct { + ProfilePublicKeyBase58Check string + ScopeType lib.LockupLimitScopeTypeString + Operation lib.LockupLimitOperationString + OpCount uint64 +} + // TransactionSpendingLimitResponse is a backend struct used to describe the TransactionSpendingLimit for a Derived key // in a way that can be JSON encoded/decoded. type TransactionSpendingLimitResponse struct { @@ -3286,6 +3293,8 @@ type TransactionSpendingLimitResponse struct { UnstakeLimitMap []UnstakeLimitMapItem // UnlockStakeLimitMap is a slice of UnlockStakeLimitMapItems UnlockStakeLimitMap []UnlockStakeLimitMapItem + // LockupLimitMap is a slice of LockupLimitMapItems + LockupLimitMap []LockupLimitMapItem // ===== ENCODER MIGRATION lib.UnlimitedDerivedKeysMigration ===== // IsUnlimited determines whether this derived key is unlimited. An unlimited derived key can perform all transactions @@ -3683,6 +3692,21 @@ func TransactionSpendingLimitToResponse( } } + if len(transactionSpendingLimit.LockupLimitMap) > 0 { + for lockupLimitKey, opCount := range transactionSpendingLimit.LockupLimitMap { + publicKeyBytes := utxoView.GetPublicKeyForPKID(&lockupLimitKey.ProfilePKID) + publicKeyBase58Check := lib.Base58CheckEncode(publicKeyBytes, false, params) + transactionSpendingLimitResponse.LockupLimitMap = append( + transactionSpendingLimitResponse.LockupLimitMap, + LockupLimitMapItem{ + ProfilePublicKeyBase58Check: publicKeyBase58Check, + ScopeType: lockupLimitKey.ScopeType.ToScopeString(), + Operation: lockupLimitKey.Operation.ToOperationString(), + OpCount: opCount, + }) + } + } + return transactionSpendingLimitResponse } @@ -3890,6 +3914,21 @@ func (fes *APIServer) TransactionSpendingLimitFromResponse( transactionSpendingLimit.UnlockStakeLimitMap[unlockStakeLimitKey] = unlockStakeLimitMapItem.OpCount } } + if len(transactionSpendingLimitResponse.LockupLimitMap) > 0 { + transactionSpendingLimit.LockupLimitMap = make(map[lib.LockupLimitKey]uint64) + for _, lockupLimitMapItem := range transactionSpendingLimitResponse.LockupLimitMap { + profilePublicKey, _, err := lib.Base58CheckDecode(lockupLimitMapItem.ProfilePublicKeyBase58Check) + if err != nil { + return nil, err + } + pkidEntry := utxoView.GetPKIDForPublicKey(profilePublicKey) + transactionSpendingLimit.LockupLimitMap[lib.MakeLockupLimitKey( + *pkidEntry.PKID, + lockupLimitMapItem.ScopeType.ToScopeType(), + lockupLimitMapItem.Operation.ToOperationType(), + )] = lockupLimitMapItem.OpCount + } + } return transactionSpendingLimit, nil } @@ -4098,11 +4137,12 @@ func (fes *APIServer) GetTransactionSpending(ww http.ResponseWriter, req *http.R func (fes *APIServer) simulateSubmitTransaction(utxoView *lib.UtxoView, txn *lib.MsgDeSoTxn) (_utxoOperations []*lib.UtxoOperation, _totalInput uint64, _totalOutput uint64, _fees uint64, _err error) { bestHeight := fes.blockchain.BlockTip().Height + 1 + bytes, _ := txn.ToBytes(false) return utxoView.ConnectTransaction( txn, txn.Hash(), bestHeight, - 0, + time.Now().UnixNano(), false, false, ) diff --git a/routes/user.go b/routes/user.go index 62a2e24d..410a94d2 100644 --- a/routes/user.go +++ b/routes/user.go @@ -641,10 +641,11 @@ type CoinEntryResponse struct { } type DAOCoinEntryResponse struct { - NumberOfHolders uint64 - CoinsInCirculationNanos uint256.Int - MintingDisabled bool - TransferRestrictionStatus TransferRestrictionStatusString + NumberOfHolders uint64 + CoinsInCirculationNanos uint256.Int + MintingDisabled bool + TransferRestrictionStatus TransferRestrictionStatusString + LockupTransferRestrictionStatus TransferRestrictionStatusString } // GetProfiles ... @@ -1060,6 +1061,8 @@ func (fes *APIServer) _profileEntryToResponse(profileEntry *lib.ProfileEntry, ut MintingDisabled: profileEntry.DAOCoinEntry.MintingDisabled, TransferRestrictionStatus: getTransferRestrictionStatusStringFromTransferRestrictionStatus( profileEntry.DAOCoinEntry.TransferRestrictionStatus), + LockupTransferRestrictionStatus: getTransferRestrictionStatusStringFromTransferRestrictionStatus( + profileEntry.DAOCoinEntry.LockupTransferRestrictionStatus), }, CoinPriceDeSoNanos: coinPriceDeSoNanos, CoinPriceBitCloutNanos: coinPriceDeSoNanos,