pancakeswapnft/nft.go

314 lines
8.1 KiB
Go

package pancakeswapnft
import (
"context"
"fmt"
"sort"
"strconv"
"sync"
"time"
"github.com/dustin/go-humanize"
"github.com/machinebox/graphql"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
type AskOrder struct {
Id string `json:"id"`
Block string `json:"block"`
AskPrice float64 `json:"askPrice,string"`
Timestamp int64 `json:"timestamp,string"`
Seller User `json:"seller"`
Nft Nft `json:"nft"`
Collection Collection `json:"collection"`
OrderType string `json:"orderType"`
}
type Nft struct {
Id string `json:"id"`
TokenID int64 `json:"tokenId,string"`
OtherId int64 `json:"otherId,string"`
Collection Collection `json:"collection"`
MetadataUrl string `json:"metadataUrl"`
UpdatedAt int64 `json:"updatedAt,string"`
CurrentAskPrice float64 `json:"currentAskPrice,string"`
CurrentSeller string `json:"currentSeller"`
CurrentSellerUsername string
LatestTradedPriceInBNB float64 `json:"latestTradedPriceInBNB,string"`
TradeVolumeBNB float64 `json:"tradeVolumeBNB,string"`
TotalTrades int64 `json:"totalTrades,string"`
TransactionHistory []Transaction `json:"transactionHistory"`
AskHistory []AskOrder `json:"askHistory"`
IsTradable bool `json:"IsTradable"`
Squad Squad
Score float64
Url string
TransactionCount int
UpdatedTimeString string
IsRecent bool
IsVeryRecent bool
IsNotMine bool
}
type MarketService struct {
collection string
graphClient *graphql.Client
squadservice *SquadService
floorService *FloorService
profileService *ProfileService
bscaddress string
nfts []Nft
floor float64
nftslock *sync.RWMutex
}
func NewMarketService(collection string, graphClient *graphql.Client, squadservice *SquadService, floorService *FloorService, profileService *ProfileService, bscadress string) *MarketService {
return &MarketService{
collection: collection,
graphClient: graphClient,
squadservice: squadservice,
floorService: floorService,
profileService: profileService,
bscaddress: bscadress,
nfts: nil,
floor: 0,
nftslock: &sync.RWMutex{},
}
}
func (s *MarketService) Start(ctx context.Context) chan struct{} {
done := make(chan struct{})
t := time.NewTicker(5 * time.Second)
go func() {
defer close(done)
err := s.updateMarket(ctx)
if err != nil {
log.Error().Err(err).Msg("updating market")
}
for {
select {
case <-ctx.Done():
return
case <-t.C:
err := s.updateMarket(ctx)
if err != nil {
log.Error().Err(err).Msg("updating market")
}
}
}
}()
return done
}
func (s *MarketService) updateMarket(ctx context.Context) error {
nfts, floor, err := s.getFirstNPages(ctx, 5)
if err != nil {
return errors.Wrap(err, "get first pages")
}
s.setNftCache(nfts, floor)
return nil
}
func (s *MarketService) GetNftCache() ([]Nft, float64) {
s.nftslock.RLock()
defer s.nftslock.RUnlock()
return s.nfts, s.floor
}
func (s *MarketService) setNftCache(nfts []Nft, floor float64) {
s.nftslock.Lock()
defer s.nftslock.Unlock()
s.nfts = nfts
s.floor = floor
}
func (s *MarketService) GetNft(ctx context.Context, collection string, tokenId int) (Nft, error) {
req := graphql.NewRequest(`
query ($where: NFT_filter) {
nfts(where: $where) {
id
tokenId
otherId
collection {
id
}
metadataUrl
currentAskPrice
currentSeller
updatedAt
latestTradedPriceInBNB
tradeVolumeBNB
totalTrades
transactionHistory {
id
block
timestamp
askPrice
netPrice
buyer {
id
}
seller {
id
}
withBNB
}
askHistory {
id
block
askPrice
timestamp
seller {
id
}
orderType
}
}
}`)
req.Var("where", struct {
Collection string `json:"collection"`
TokenId string `json:"tokenId"`
}{
Collection: collection,
TokenId: strconv.Itoa(tokenId),
})
var respData struct {
Nfts []Nft `json:"nfts"`
}
if err := s.graphClient.Run(ctx, req, &respData); err != nil {
return Nft{}, errors.Wrap(err, "querying graphql")
}
if len(respData.Nfts) <= 0 {
return Nft{}, errors.Errorf("no nft found for id %d", tokenId)
}
res := respData.Nfts[0]
sq, err := s.squadservice.GetSquad(res.TokenID)
if err != nil {
return Nft{}, errors.Wrap(err, "getting squad")
}
res.Squad = sq
res.Score = 10 / ((float64(sq.Rank) / float64(10000)) * res.CurrentAskPrice)
res.Url = fmt.Sprintf("https://pancakeswap.finance/nfts/collections/%s/%d", collection, res.TokenID)
res.TransactionCount = len(res.TransactionHistory)
sort.Slice(res.TransactionHistory, func(i, j int) bool {
return res.TransactionHistory[i].Timestamp > res.TransactionHistory[j].Timestamp
})
res.UpdatedTimeString = humanize.Time(time.Unix(res.UpdatedAt, 0))
res.IsRecent = time.Unix(res.UpdatedAt, 0).After(time.Now().Add(-1 * time.Hour))
res.IsVeryRecent = time.Unix(res.UpdatedAt, 0).After(time.Now().Add(-5 * time.Minute))
res.IsNotMine = res.CurrentSeller != s.bscaddress
return res, nil
}
func (s *MarketService) getPage(ctx context.Context, collection string, pagenum int) ([]Nft, float64, error) {
req := graphql.NewRequest(`
query ($first: Int, $skip: Int!, $where: NFT_filter, $orderBy: NFT_orderBy, $orderDirection: OrderDirection) {
nfts(where: $where, first: $first, orderBy: $orderBy, orderDirection: $orderDirection, skip: $skip) {
tokenId
currentAskPrice
currentSeller
updatedAt
transactionHistory {
timestamp
askPrice
}
}
}`)
req.Var("first", 1000)
req.Var("orderBy", "currentAskPrice")
req.Var("orderDirection", "asc")
req.Var("skip", (pagenum-1)*1000)
req.Var("where", struct {
Collection string `json:"collection"`
IsTradable bool `json:"isTradable"`
}{
Collection: collection,
IsTradable: true,
})
var respData struct {
Nfts []Nft `json:"nfts"`
}
if err := s.graphClient.Run(ctx, req, &respData); err != nil {
return nil, 0, errors.Wrap(err, "querying graphql")
}
var res []Nft
floor := float64(0)
for _, nft := range respData.Nfts {
squad, err := s.squadservice.GetSquad(nft.TokenID)
if err != nil {
log.Error().Err(err).Msgf("getting squad for tokenid %d", nft.TokenID)
continue
}
nft.Squad = squad
nft.Score = nft.Squad.GetScoreByPrice(s.floorService, nft.CurrentAskPrice)
nft.Url = fmt.Sprintf("https://pancakeswap.finance/nfts/collections/%s/%d", collection, nft.TokenID)
nft.TransactionCount = len(nft.TransactionHistory)
sort.Slice(nft.TransactionHistory, func(i, j int) bool {
return nft.TransactionHistory[i].Timestamp > nft.TransactionHistory[j].Timestamp
})
nft.UpdatedTimeString = humanize.Time(time.Unix(nft.UpdatedAt, 0))
nft.IsRecent = time.Unix(nft.UpdatedAt, 0).After(time.Now().Add(-1 * time.Hour))
nft.IsVeryRecent = time.Unix(nft.UpdatedAt, 0).After(time.Now().Add(-5 * time.Minute))
if floor == 0 || nft.CurrentAskPrice < floor {
floor = nft.CurrentAskPrice
}
nft.IsNotMine = nft.CurrentSeller != s.bscaddress
res = append(res, nft)
}
return res, floor, nil
}
func (s *MarketService) getFirstNPages(ctx context.Context, n int) ([]Nft, float64, error) {
var res []Nft
var floor float64
for i := 1; i <= n; i++ {
page, tmpfloor, err := s.getPage(ctx, s.collection, i)
if err != nil {
return nil, 0, errors.Wrapf(err, "getting page %d", i)
}
res = append(res, page...)
if floor == 0 || tmpfloor < floor {
floor = tmpfloor
}
}
return res, floor, nil
}
func (s *MarketService) getAll(ctx context.Context) ([]Nft, float64, error) {
var res []Nft
var floor float64
currentpage := 1
for {
page, tmpfloor, err := s.getPage(ctx, s.collection, currentpage)
if err != nil {
return nil, 0, errors.Wrapf(err, "getting page %d", currentpage)
}
if len(page) == 0 {
break
}
res = append(res, page...)
if floor == 0 || tmpfloor < floor {
floor = tmpfloor
}
currentpage++
}
return res, floor, nil
}