Skip to content

Commit

Permalink
Add endpoints for admin management of NFT drops.
Browse files Browse the repository at this point in the history
  • Loading branch information
redpartyhat committed Jul 16, 2021
1 parent d37a92d commit 50d97b4
Show file tree
Hide file tree
Showing 3 changed files with 380 additions and 18 deletions.
327 changes: 327 additions & 0 deletions routes/admin_nft.go
@@ -0,0 +1,327 @@
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
}

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) 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
profileEntryResponseMap := make(map[lib.PkMapKey]*ProfileEntryResponse)
if dropEntryToReturn != nil {
// Grab a view (needed for getting global params, etc).
utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView()
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("AdminGetNFTDrop: Error getting utxoView: %v", err))
return
}

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

// 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 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
}

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
}

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

if err = json.NewEncoder(ww).Encode(res); err != nil {
_AddInternalServerError(ww, fmt.Sprintf("AdminUpdateNFTDrop: Problem serializing object to JSON: %v", err))
return
}
}
23 changes: 20 additions & 3 deletions routes/global_state.go
Expand Up @@ -4,11 +4,12 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/bitclout/core/lib"
"io"
"net/http"
"strings"

"github.com/bitclout/core/lib"

"github.com/dgraph-io/badger/v3"
"github.com/nyaruka/phonenumbers"
"github.com/pkg/errors"
Expand Down Expand Up @@ -138,13 +139,23 @@ var (

_GlobalStatePrefixBuyBitCloutFeeBasisPoints = []byte{16}

// NFT drop info.
_GlobalStatePrefixNFTDropNumberToNFTDropEntry = []byte{17}

// TODO: This process is a bit error-prone. We should come up with a test or
// something to at least catch cases where people have two prefixes with the
// same ID.
//
// NEXT_TAG: 17
// NEXT_TAG: 18
)

type NFTDropEntry struct {
IsActive bool
DropNumber uint64
DropTstampNanos uint64
NFTHashes []*lib.BlockHash
}

// This struct contains all the metadata associated with a user's public key.
type UserMetadata struct {
// The PublicKey of the user this metadata is associated with.
Expand Down Expand Up @@ -229,6 +240,13 @@ type WyreWalletOrderMetadata struct {
BasicTransferTxnBlockHash *lib.BlockHash
}

func GlobalStateKeyForNFTDropEntry(dropNumber uint64) []byte {
dropNumBytes := lib.EncodeUint64(uint64(dropNumber))
keyBytes := _GlobalStatePrefixNFTDropNumberToNFTDropEntry
keyBytes = append(keyBytes, dropNumBytes...)
return keyBytes
}

// countryCode is a string like 'US' (Note: the phonenumbers lib calls this a "region code")
func GlobalStateKeyForPhoneNumberStringToPhoneNumberMetadata(phoneNumber string) (_key []byte, _err error) {
parsedNumber, err := phonenumbers.Parse(phoneNumber, "")
Expand Down Expand Up @@ -356,7 +374,6 @@ func GlobalStateKeyForBuyBitCloutFeeBasisPoints() []byte {
return prefixCopy
}


type GlobalStatePutRemoteRequest struct {
Key []byte
Value []byte
Expand Down

0 comments on commit 50d97b4

Please sign in to comment.