Skip to content

Commit

Permalink
Rph/nifties (#87)
Browse files Browse the repository at this point in the history
* Begin adding endpoints for NFTs.

* Add endpoint to update the status of an NFT.

* Add endpoint to create an NFT bid.

* Add endpoint to accept an NFT bid.

* Add more scaffolding for NFT endpoints.

* Clean up broken endpoints.

* Fix backend to get MaxCopiesPerNFT from the GlobalParamsEntry not from the constants.

* Add IsNFT to PostEntryResponse.

* Add create-nft route.

* Add wirings for update-nft, create-nft-bid, accept-nft.

* Ln/nifties (#71)

* Fix nil pointer error on invalid file upload

* Add lock to seed BitClout and monkey-patch blockchain.com last_price bug

* Trigger rebuild

* Trigger rebuild

* return graylist and blacklist status in get-single-profile request (#55)

* Ln/send seed bitclout (#56)

* retry sending BitClout once in the event of an error after sleeping for 5 seconds. glog the errors

* more glog

* add more details in error logs when send seed bitclout fails

* Don't use the 24h price when fetching from Blockchain.com

* Track last hour of pricing data (#59)

* keep one hours worth of last trade price history and take the max of those values

* add more comments to explain the culling logic

* add datadog logging

* [stable] Release 1.0.4

* Move github actions to buildkite

* Upgrade badgerdb

* [stable] Release 1.0.5

* rename WyreBTCAddress to BuyBitCloutBTCAddress to more accurately reflect this flag's purpose (#61)

* Ln/txn ids only (#62)

* add support for IDsOnly as described in the documentation

* only set the TransactionIdBase58Check in the Transactions slice

* omit transaction response fields if empty

* [stable] Release 1.0.6

* fix IDsOnly mempool transactions for public key (#64)

* add get nfts for user logic

Co-authored-by: maebeam <[email protected]>
Co-authored-by: diamondhands0 <[email protected]>

* fix get nfts for user

* add profile entry response to nft entry response

* Add endpoints for admin management of NFT drops.

* fix profile entry response on get nfts for user endpoint

* Initial wiring of get-nft-marketplace endpoint.

* fix computation of nft creation fee, make a non-admin version of the get-global-params endpoint

* improve performance of existing endpoints by deduplicating data and omitting empty attributes, add get nft collection summary and get nft bids for user endpoints

* Add endpoint to get next NFT showcase timestamp for showcase countdown timer.

* Pass NumNFTCopiesForSale through to the frontend on the postEntry.

* add high and low bid to bid entry response to support showing whether or not this bid is currently in the lead, fix GetNFTsForUser when isForSale is nil

* add BidderBalanceNanos to bid entry response so frontend can apply logic to handle bids that can't be accepted with bidder's current balance

* Add drop entry posts to the update NFT drop endepoint.

* add GetNFTEntriesForPostHash endpoint

* use GetNFTEntriesForPostHash

* fix error text

* fix create update global params txn call in admin_transaction.go

* Improve NFT error message

Co-authored-by: redpartyhat <[email protected]>
Co-authored-by: maebeam <[email protected]>
Co-authored-by: diamondhands0 <[email protected]>
  • Loading branch information
4 people authored Jul 28, 2021
1 parent 6270d14 commit 95a732d
Show file tree
Hide file tree
Showing 7 changed files with 1,908 additions and 39 deletions.
349 changes: 349 additions & 0 deletions routes/admin_nft.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
package routes

import (
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"time"

"github.com/bitclout/core/lib"
)

type AdminGetNFTDropRequest struct {
// "-1" is used to request the next planned drop.
DropNumber int `safeForLogging:"true"`
}

type AdminGetNFTDropResponse struct {
DropEntry *NFTDropEntry
Posts []*PostEntryResponse
}

// Check global state and get the latest drop entry if available.
// If no drop entry is found in global state, returns a default-initialized NFTDropEntry.
func (fes *APIServer) GetLatestNFTDropEntry() (_dropEntry *NFTDropEntry, _err error) {
seekKey := _GlobalStatePrefixNFTDropNumberToNFTDropEntry
maxKeyLen := 9 // These keys are 1 prefix byte + 8 bytes for the uint64 drop number.
_, vals, err := fes.GlobalStateSeek(seekKey, seekKey, maxKeyLen, 1, true, true)
if err != nil {
return nil, fmt.Errorf("AdminGetNFTDrop: Error getting latest drop: %v", err)
}

if len(vals) > 1 {
return nil, fmt.Errorf(
"AdminGetNFTDrop: Unexpected number of drop entries (%d) returned.", len(vals))
}

dropEntry := &NFTDropEntry{}
if len(vals) != 0 {
// If we got here, we found a drop entry. Save the bytes to decode later.
dropEntryBytes := vals[0]
err = gob.NewDecoder(bytes.NewReader(dropEntryBytes)).Decode(&dropEntry)
if err != nil {
return nil, fmt.Errorf("AdminGetNFTDrop: Problem decoding bytes for latest drop entry: %v", err)
}
}

return dropEntry, nil
}

func (fes *APIServer) GetNFTDropEntry(nftDropNumber uint64) (_dropEntry *NFTDropEntry, _err error) {
keyBytes := GlobalStateKeyForNFTDropEntry(uint64(nftDropNumber))
dropEntryBytes, err := fes.GlobalStateGet(keyBytes)
if err != nil {
return nil, fmt.Errorf("GetNFTDropEntry: %v", err)
}

dropEntry := &NFTDropEntry{}
err = gob.NewDecoder(bytes.NewReader(dropEntryBytes)).Decode(&dropEntry)
if err != nil {
return nil, fmt.Errorf("GetNFTDropEntry: %v", err)
}

return dropEntry, nil
}

func (fes *APIServer) GetPostsForNFTDropEntry(dropEntryToReturn *NFTDropEntry,
) (_posts []*PostEntryResponse, _err error) {
profileEntryResponseMap := make(map[lib.PkMapKey]*ProfileEntryResponse)
var postEntryResponses []*PostEntryResponse

// Grab a view (needed for getting global params, etc).
utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView()
if err != nil {
return nil, fmt.Errorf("AdminGetPostsForNFTDropEntry: Error getting utxoView: %v", err)
}

for _, postHash := range dropEntryToReturn.NFTHashes {
postEntry := utxoView.GetPostEntryForPostHash(postHash)
postEntryResponse, err := fes._postEntryToResponse(postEntry, false, fes.Params, utxoView, nil, 2)
if err != nil {
return nil, fmt.Errorf(
"AdminGetPostsForNFTDropEntry: Error building postEntryResponse: %v, %s", err, postHash.String())
}

// Add the profile entry to the post entry.
profileEntryResponse, entryFound := profileEntryResponseMap[lib.MakePkMapKey(postEntry.PosterPublicKey)]
if !entryFound {
// If we didn't find the entry in our map, we need to make it...
profileEntry := utxoView.GetProfileEntryForPublicKey(postEntry.PosterPublicKey)
if profileEntry == nil {
// If we didn't find a profile entry, skip this post.
continue
} else {
profileEntryResponse = _profileEntryToResponse(profileEntry, fes.Params, nil, utxoView)
profileEntryResponseMap[lib.MakePkMapKey(postEntry.PosterPublicKey)] = profileEntryResponse
}
}
postEntryResponse.ProfileEntryResponse = profileEntryResponse

postEntryResponses = append(postEntryResponses, postEntryResponse)
}

return postEntryResponses, nil
}

func (fes *APIServer) AdminGetNFTDrop(ww http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes))
requestData := AdminGetNFTDropRequest{}
if err := decoder.Decode(&requestData); err != nil {
_AddBadRequestError(ww, fmt.Sprintf("AdminGetNFTDrop: Error parsing request body: %v", err))
return
}

var err error
var dropEntryToReturn *NFTDropEntry
if requestData.DropNumber < 0 {
dropEntryToReturn, err = fes.GetLatestNFTDropEntry()
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("AdminGetNFTDrop: Error getting latest drop: %v", err))
return
}
} else {
// Look up the drop entry for the drop number given.
dropEntryToReturn, err = fes.GetNFTDropEntry(uint64(requestData.DropNumber))
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf(
"AdminGetNFTDrop: Error getting NFT drop #%d: %v", requestData.DropNumber, err))
return
}
}

// Note that "dropEntryToReturn" can be nil if there are no entries in global state.
var postEntryResponses []*PostEntryResponse
if dropEntryToReturn != nil {
postEntryResponses, err = fes.GetPostsForNFTDropEntry(dropEntryToReturn)
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("AdminGetNFTDrop: : %v", err))
return
}
}

// Return all the data associated with the transaction in the response
res := AdminGetNFTDropResponse{
DropEntry: dropEntryToReturn,
Posts: postEntryResponses,
}

if err = json.NewEncoder(ww).Encode(res); err != nil {
_AddInternalServerError(ww, fmt.Sprintf("AdminGetNFTDrop: Problem serializing object to JSON: %v", err))
return
}
}

type AdminUpdateNFTDropRequest struct {
DropNumber int `safeForLogging:"true"`
DropTstampNanos int `safeForLogging:"true"`
IsActive bool `safeForLogging:"true"`
NFTHashHexToAdd string `safeForLogging:"true"`
NFTHashHexToRemove string `safeForLogging:"true"`
}

type AdminUpdateNFTDropResponse struct {
DropEntry *NFTDropEntry
Posts []*PostEntryResponse
}

func (fes *APIServer) AdminUpdateNFTDrop(ww http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(io.LimitReader(req.Body, MaxRequestBodySizeBytes))
requestData := AdminUpdateNFTDropRequest{}
err := decoder.Decode(&requestData)
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("AdminUpdateNFTDrop: Error parsing request body: %v", err))
return
}

if requestData.DropNumber < 1 {
_AddBadRequestError(ww, fmt.Sprintf(
"AdminUpdateNFTDrop: Drop number must be greater than zero, received: %d", requestData.DropNumber))
return
}

if requestData.DropTstampNanos < 0 {
_AddBadRequestError(ww, fmt.Sprintf(
"AdminUpdateNFTDrop: Drop timestamp cannot be negative, received: %d", requestData.DropTstampNanos))
return
}

if requestData.NFTHashHexToAdd != "" && requestData.NFTHashHexToRemove != "" {
_AddBadRequestError(ww, fmt.Sprint(
"AdminUpdateNFTDrop: Cannot add and remove an NFT in the same operation."))
return
}

var latestDropEntry *NFTDropEntry
latestDropEntry, err = fes.GetLatestNFTDropEntry()
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("AdminUpdateNFTDrop: Error getting latest drop: %v", err))
return
}

// Now for the business.
var updatedDropEntry *NFTDropEntry
currentTime := uint64(time.Now().UnixNano())
if uint64(requestData.DropNumber) > latestDropEntry.DropNumber {
// If we make it here, we are making a new drop. Run some checks to make sure that the
// timestamp provided make sense.
if latestDropEntry.DropTstampNanos > currentTime {
_AddBadRequestError(ww, fmt.Sprint(
"AdminUpdateNFTDrop: Cannot create a new drop when one is already pending."))
return
}
if uint64(requestData.DropTstampNanos) < currentTime {
_AddBadRequestError(ww, fmt.Sprint(
"AdminUpdateNFTDrop: Cannot create a new drop with a tstamp in the past."))
return
}
if uint64(requestData.DropTstampNanos) < latestDropEntry.DropTstampNanos {
_AddBadRequestError(ww, fmt.Sprint(
"AdminUpdateNFTDrop: Cannot create a new drop with a tstamp before the previous drop."))
return
}

// Regardless of the drop number provided, we force the new drop to be the previous number + 1.
updatedDropEntry = &NFTDropEntry{
DropNumber: uint64(latestDropEntry.DropNumber + 1),
DropTstampNanos: uint64(requestData.DropTstampNanos),
}

} else {
// In this case, we are updating an existing drop.
updatedDropEntry = latestDropEntry
if uint64(requestData.DropNumber) != latestDropEntry.DropNumber {
updatedDropEntry, err = fes.GetNFTDropEntry(uint64(requestData.DropNumber))
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf(
"AdminUpdateNFTDrop: Error getting drop #%d: %v", requestData.DropNumber, err))
return
}
}

// There are only two possible drops that can be updated (you can't update past drops):
// - The current "active" drop.
// - The next "pending" drop.
canUpdateDrop := false
latestDropIsPending := latestDropEntry.DropTstampNanos > currentTime
if latestDropIsPending && uint64(requestData.DropNumber) >= latestDropEntry.DropNumber-1 {
// In this case their is a pending drop so the latest drop and the previous drop are editable.
canUpdateDrop = true
} else if !latestDropIsPending && uint64(requestData.DropNumber) == latestDropEntry.DropNumber {
// In this case there is no pending drop so you can only update the latest drop.
canUpdateDrop = true
}

if !canUpdateDrop {
_AddBadRequestError(ww, fmt.Sprintf(
"AdminUpdateNFTDrop: Cannot edit past drop #%d.", requestData.DropNumber))
return
}

// Update IsActive.
updatedDropEntry.IsActive = requestData.IsActive

// Consider updating DropTstampNanos.
if uint64(requestData.DropTstampNanos) > currentTime &&
uint64(requestData.DropNumber) == latestDropEntry.DropNumber {
updatedDropEntry.DropTstampNanos = uint64(requestData.DropTstampNanos)

} else if uint64(requestData.DropTstampNanos) != updatedDropEntry.DropTstampNanos {
_AddBadRequestError(ww, fmt.Sprintf(
"AdminUpdateNFTDrop: Can only update latest drop with tstamp in the future."))
return
}

utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView()
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("AdminUpdateNFTDrop: Error getting utxoView: %v", err))
return
}

// Add new NFT hashes.
if requestData.NFTHashHexToAdd != "" {
// Decode the hash and make sure it is a valid NFT so that we can add it to the entry.
postHash, err := GetPostHashFromPostHashHex(requestData.NFTHashHexToAdd)
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("AdminUpdateNFTDrop: Error getting post hash: %v", err))
return
}
postEntry := utxoView.GetPostEntryForPostHash(postHash)
if !postEntry.IsNFT {
_AddBadRequestError(ww, fmt.Sprintf(
"AdminUpdateNFTDrop: Cannot add non-NFT to drop: %v", postHash.String()))
return
}

updatedDropEntry.NFTHashes = append(updatedDropEntry.NFTHashes, postHash)
}

// Remove unwanted NFT hashes.
if requestData.NFTHashHexToRemove != "" {
// Decode the hash and make sure it is a valid NFT.
nftHashToRemove, err := GetPostHashFromPostHashHex(requestData.NFTHashHexToRemove)
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf(
"AdminUpdateNFTDrop: Error getting post hash to remove: %v", err))
return
}

for nftHashIdx, nftHash := range updatedDropEntry.NFTHashes {
if reflect.DeepEqual(nftHash, nftHashToRemove) {
updatedDropEntry.NFTHashes = append(
updatedDropEntry.NFTHashes[:nftHashIdx], updatedDropEntry.NFTHashes[nftHashIdx+1:]...)
break
}
}
}
}

// Set the updated drop entry.
globalStateKey := GlobalStateKeyForNFTDropEntry(uint64(requestData.DropNumber))
updatedDropEntryBuf := bytes.NewBuffer([]byte{})
gob.NewEncoder(updatedDropEntryBuf).Encode(updatedDropEntry)
err = fes.GlobalStatePut(globalStateKey, updatedDropEntryBuf.Bytes())
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("AdminUpdateNFTDrop: Error encoding updated drop: %v", err))
return
}

// Note that "dropEntryToReturn" can be nil if there are no entries in global state.
postEntryResponses, err := fes.GetPostsForNFTDropEntry(updatedDropEntry)
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("AdminGetNFTDrop: : %v", err))
return
}

// Return all the data associated with the transaction in the response
res := AdminUpdateNFTDropResponse{
DropEntry: updatedDropEntry,
Posts: postEntryResponses,
}

if err = json.NewEncoder(ww).Encode(res); err != nil {
_AddInternalServerError(ww, fmt.Sprintf("AdminUpdateNFTDrop: Problem serializing object to JSON: %v", err))
return
}
}
Loading

0 comments on commit 95a732d

Please sign in to comment.